mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add version_resolved_in to software API (#13939)
This commit is contained in:
parent
c508209e11
commit
338c64d78b
1
changes/11666-add-nvd-resolved-version
Normal file
1
changes/11666-add-nvd-resolved-version
Normal file
@ -0,0 +1 @@
|
||||
- (premium only) adds `resolved_in_version` to `/fleet/software` APIs pulled from NVD feed
|
@ -0,0 +1,27 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20230918132351, Down_20230918132351)
|
||||
}
|
||||
|
||||
func Up_20230918132351(tx *sql.Tx) error {
|
||||
stmt := `
|
||||
ALTER TABLE software_cve
|
||||
ADD COLUMN resolved_in_version VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL;
|
||||
`
|
||||
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("add resolved_in_version column to software_cve: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20230918132351(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20230918132351(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
insertStmt := `
|
||||
INSERT INTO software_cve (
|
||||
software_id,
|
||||
source,
|
||||
cve
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
args := []interface{}{
|
||||
1,
|
||||
0,
|
||||
"CVE-2021-1234",
|
||||
}
|
||||
|
||||
execNoErr(t, db, insertStmt, args...)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// check for null resolved_in_version default
|
||||
selectAndAssert(t, db, 1, 0, "CVE-2021-1234", nil)
|
||||
|
||||
// Update the resolved_in_version and verify
|
||||
updateVersion := "6.0.2-76060002.202210150739~1666289067~22.04~fe0ce53" // long string to test the capacity of the new column
|
||||
updateStmt := `
|
||||
UPDATE software_cve
|
||||
SET resolved_in_version = ?
|
||||
WHERE software_id = ? AND source = ? AND cve = ?
|
||||
`
|
||||
|
||||
execNoErr(t, db, updateStmt, updateVersion, 1, 0, "CVE-2021-1234")
|
||||
selectAndAssert(t, db, 1, 0, "CVE-2021-1234", &updateVersion)
|
||||
|
||||
// Insert a new record and verify
|
||||
insertStmt = `
|
||||
INSERT INTO software_cve (
|
||||
software_id,
|
||||
source,
|
||||
cve,
|
||||
resolved_in_version
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
execNoErr(t, db, insertStmt, 1, 0, "CVE-2021-1235", updateVersion)
|
||||
selectAndAssert(t, db, 1, 0, "CVE-2021-1235", &updateVersion)
|
||||
}
|
||||
|
||||
func selectAndAssert(t *testing.T, db *sqlx.DB, softwareID uint, source uint, cve string, resolvedInVersion *string) {
|
||||
var softwareCVE struct {
|
||||
SoftwareID uint `db:"software_id"`
|
||||
Source uint `db:"source"`
|
||||
CVE string `db:"cve"`
|
||||
ResolvedInVersion *string `db:"resolved_in_version"`
|
||||
}
|
||||
|
||||
selectStmt := `
|
||||
SELECT software_id, source, cve, resolved_in_version
|
||||
FROM software_cve
|
||||
WHERE software_id = ? AND source = ? AND cve = ?
|
||||
`
|
||||
|
||||
require.NoError(t, db.Get(&softwareCVE, selectStmt, softwareID, source, cve))
|
||||
require.Equal(t, softwareID, softwareCVE.SoftwareID)
|
||||
require.Equal(t, source, softwareCVE.Source)
|
||||
require.Equal(t, cve, softwareCVE.CVE)
|
||||
require.Equal(t, resolvedInVersion, softwareCVE.ResolvedInVersion)
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -588,6 +588,7 @@ func listSoftwareDB(
|
||||
cve.CISAKnownExploit = &result.CISAKnownExploit
|
||||
cve.CVEPublished = &result.CVEPublished
|
||||
cve.Description = &result.Description
|
||||
cve.ResolvedInVersion = &result.ResolvedInVersion
|
||||
}
|
||||
softwares[idx].Vulnerabilities = append(softwares[idx].Vulnerabilities, cve)
|
||||
}
|
||||
@ -597,14 +598,33 @@ func listSoftwareDB(
|
||||
}
|
||||
|
||||
// softwareCVE is used for left joins with cve
|
||||
//
|
||||
//
|
||||
|
||||
type softwareCVE struct {
|
||||
fleet.Software
|
||||
CVE *string `db:"cve"`
|
||||
CVSSScore *float64 `db:"cvss_score"`
|
||||
EPSSProbability *float64 `db:"epss_probability"`
|
||||
CISAKnownExploit *bool `db:"cisa_known_exploit"`
|
||||
CVEPublished *time.Time `db:"cve_published"`
|
||||
Description *string `db:"description"`
|
||||
|
||||
// CVE is the CVE identifier pulled from the NVD json (e.g. CVE-2019-1234)
|
||||
CVE *string `db:"cve"`
|
||||
|
||||
// CVSSScore is the CVSS score pulled from the NVD json (premium only)
|
||||
CVSSScore *float64 `db:"cvss_score"`
|
||||
|
||||
// EPSSProbability is the EPSS probability pulled from FIRST (premium only)
|
||||
EPSSProbability *float64 `db:"epss_probability"`
|
||||
|
||||
// CISAKnownExploit is the CISAKnownExploit pulled from CISA (premium only)
|
||||
CISAKnownExploit *bool `db:"cisa_known_exploit"`
|
||||
|
||||
// CVEPublished is the CVE published date pulled from the NVD json (premium only)
|
||||
CVEPublished *time.Time `db:"cve_published"`
|
||||
|
||||
// Description is the CVE description field pulled from the NVD json
|
||||
Description *string `db:"description"`
|
||||
|
||||
// ResolvedInVersion is the version of software where the CVE is no longer applicable.
|
||||
// This is pulled from the versionEndExcluding field in the NVD json
|
||||
ResolvedInVersion *string `db:"resolved_in_version"`
|
||||
}
|
||||
|
||||
func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, error) {
|
||||
@ -694,11 +714,12 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e
|
||||
goqu.On(goqu.I("c.cve").Eq(goqu.I("scv.cve"))),
|
||||
).
|
||||
SelectAppend(
|
||||
goqu.MAX("c.cvss_score").As("cvss_score"), // for ordering
|
||||
goqu.MAX("c.epss_probability").As("epss_probability"), // for ordering
|
||||
goqu.MAX("c.cisa_known_exploit").As("cisa_known_exploit"), // for ordering
|
||||
goqu.MAX("c.published").As("cve_published"), // for ordering
|
||||
goqu.MAX("c.description").As("description"), // for ordering
|
||||
goqu.MAX("c.cvss_score").As("cvss_score"), // for ordering
|
||||
goqu.MAX("c.epss_probability").As("epss_probability"), // for ordering
|
||||
goqu.MAX("c.cisa_known_exploit").As("cisa_known_exploit"), // for ordering
|
||||
goqu.MAX("c.published").As("cve_published"), // for ordering
|
||||
goqu.MAX("c.description").As("description"), // for ordering
|
||||
goqu.MAX("scv.resolved_in_version").As("resolved_in_version"), // for ordering
|
||||
)
|
||||
}
|
||||
|
||||
@ -768,6 +789,7 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e
|
||||
"c.cisa_known_exploit",
|
||||
"c.description",
|
||||
goqu.I("c.published").As("cve_published"),
|
||||
"scv.resolved_in_version",
|
||||
)
|
||||
}
|
||||
|
||||
@ -1068,6 +1090,7 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, includeCVEScores
|
||||
"c.cisa_known_exploit",
|
||||
"c.description",
|
||||
goqu.I("c.published").As("cve_published"),
|
||||
"scv.resolved_in_version",
|
||||
)
|
||||
}
|
||||
|
||||
@ -1109,6 +1132,7 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, includeCVEScores
|
||||
cve.EPSSProbability = &result.EPSSProbability
|
||||
cve.CISAKnownExploit = &result.CISAKnownExploit
|
||||
cve.CVEPublished = &result.CVEPublished
|
||||
cve.ResolvedInVersion = &result.ResolvedInVersion
|
||||
}
|
||||
software.Vulnerabilities = append(software.Vulnerabilities, cve)
|
||||
}
|
||||
@ -1408,8 +1432,15 @@ func (ds *Datastore) InsertSoftwareVulnerability(
|
||||
|
||||
var args []interface{}
|
||||
|
||||
stmt := `INSERT INTO software_cve (cve, source, software_id) VALUES (?,?,?) ON DUPLICATE KEY UPDATE updated_at=?`
|
||||
args = append(args, vuln.CVE, source, vuln.SoftwareID, time.Now().UTC())
|
||||
stmt := `
|
||||
INSERT INTO software_cve (cve, source, software_id, resolved_in_version)
|
||||
VALUES (?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
source = VALUES(source),
|
||||
resolved_in_version = VALUES(resolved_in_version),
|
||||
updated_at=?
|
||||
`
|
||||
args = append(args, vuln.CVE, source, vuln.SoftwareID, vuln.ResolvedInVersion, time.Now().UTC())
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
@ -1444,6 +1475,7 @@ func (ds *Datastore) ListSoftwareVulnerabilitiesByHostIDsSource(
|
||||
goqu.I("hs.host_id"),
|
||||
goqu.I("sc.software_id"),
|
||||
goqu.I("sc.cve"),
|
||||
goqu.I("sc.resolved_in_version"),
|
||||
).
|
||||
Where(
|
||||
goqu.I("hs.host_id").In(hostIDs),
|
||||
|
@ -548,9 +548,9 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
|
||||
vulns := []fleet.SoftwareVulnerability{
|
||||
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0001"},
|
||||
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0002"},
|
||||
{SoftwareID: host3.Software[0].ID, CVE: "CVE-2022-0003"},
|
||||
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0001", ResolvedInVersion: "2.0.0"},
|
||||
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0002", ResolvedInVersion: "2.0.0"},
|
||||
{SoftwareID: host3.Software[0].ID, CVE: "CVE-2022-0003", ResolvedInVersion: "2.0.0"},
|
||||
}
|
||||
|
||||
for _, v := range vulns {
|
||||
@ -595,22 +595,24 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
|
||||
GenerateCPE: "somecpe",
|
||||
Vulnerabilities: fleet.Vulnerabilities{
|
||||
{
|
||||
CVE: "CVE-2022-0001",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001",
|
||||
CVSSScore: ptr.Float64Ptr(2.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.01),
|
||||
CISAKnownExploit: ptr.BoolPtr(false),
|
||||
CVEPublished: ptr.TimePtr(now.Add(-2 * time.Hour)),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0001"),
|
||||
CVE: "CVE-2022-0001",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001",
|
||||
CVSSScore: ptr.Float64Ptr(2.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.01),
|
||||
CISAKnownExploit: ptr.BoolPtr(false),
|
||||
CVEPublished: ptr.TimePtr(now.Add(-2 * time.Hour)),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0001"),
|
||||
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2022-0002",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002",
|
||||
CVSSScore: ptr.Float64Ptr(1.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.99),
|
||||
CISAKnownExploit: ptr.BoolPtr(false),
|
||||
CVEPublished: ptr.TimePtr(now),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0002"),
|
||||
CVE: "CVE-2022-0002",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002",
|
||||
CVSSScore: ptr.Float64Ptr(1.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.99),
|
||||
CISAKnownExploit: ptr.BoolPtr(false),
|
||||
CVEPublished: ptr.TimePtr(now),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0002"),
|
||||
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -624,13 +626,14 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
|
||||
GenerateCPE: "somecpe2",
|
||||
Vulnerabilities: fleet.Vulnerabilities{
|
||||
{
|
||||
CVE: "CVE-2022-0003",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0003",
|
||||
CVSSScore: ptr.Float64Ptr(3.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.98),
|
||||
CISAKnownExploit: ptr.BoolPtr(true),
|
||||
CVEPublished: ptr.TimePtr(now.Add(-1 * time.Hour)),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0003"),
|
||||
CVE: "CVE-2022-0003",
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0003",
|
||||
CVSSScore: ptr.Float64Ptr(3.0),
|
||||
EPSSProbability: ptr.Float64Ptr(0.98),
|
||||
CISAKnownExploit: ptr.BoolPtr(true),
|
||||
CVEPublished: ptr.TimePtr(now.Add(-1 * time.Hour)),
|
||||
Description: ptr.StringPtr("this is a description for CVE-2022-0003"),
|
||||
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1810,6 +1813,46 @@ func testInsertSoftwareVulnerability(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, 1, occurrence["cve-1"])
|
||||
require.Equal(t, 1, occurrence["cve-2"])
|
||||
})
|
||||
|
||||
t.Run("vulnerability includes version range", func(t *testing.T) {
|
||||
// new host
|
||||
host := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
||||
|
||||
// new software
|
||||
software := fleet.Software{
|
||||
Name: "host3software", Version: "0.0.1", Source: "chrome_extensions",
|
||||
}
|
||||
|
||||
_, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{software})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
||||
|
||||
// new software cpe
|
||||
cpes := []fleet.SoftwareCPE{
|
||||
{SoftwareID: host.Software[0].ID, CPE: "cpe:2.3:a:foo:foo:0.0.1:*:*:*:*:*:*:*"},
|
||||
}
|
||||
|
||||
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// new vulnerability
|
||||
vuln := fleet.SoftwareVulnerability{
|
||||
SoftwareID: host.Software[0].ID,
|
||||
CVE: "cve-3",
|
||||
ResolvedInVersion: "1.2.3",
|
||||
}
|
||||
|
||||
inserted, err := ds.InsertSoftwareVulnerability(ctx, vuln, fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
require.True(t, inserted)
|
||||
|
||||
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, storedVulns[host.ID], 1)
|
||||
require.Equal(t, "cve-3", storedVulns[host.ID][0].CVE)
|
||||
require.Equal(t, "1.2.3", storedVulns[host.ID][0].ResolvedInVersion)
|
||||
})
|
||||
}
|
||||
|
||||
func testListCVEs(t *testing.T, ds *Datastore) {
|
||||
|
@ -12,11 +12,12 @@ type CVE struct {
|
||||
// 1. omitted when using the free tier
|
||||
// 2. null when using the premium tier, but there is no value available. This may be due to an issue with syncing cve scores.
|
||||
// 3. non-null when using the premium tier, and value is available.
|
||||
CVSSScore **float64 `json:"cvss_score,omitempty" db:"cvss_score"`
|
||||
EPSSProbability **float64 `json:"epss_probability,omitempty" db:"epss_probability"`
|
||||
CISAKnownExploit **bool `json:"cisa_known_exploit,omitempty" db:"cisa_known_exploit"`
|
||||
CVEPublished **time.Time `json:"cve_published,omitempty" db:"cve_published"`
|
||||
Description **string `json:"cve_description,omitempty" db:"description"`
|
||||
CVSSScore **float64 `json:"cvss_score,omitempty" db:"cvss_score"`
|
||||
EPSSProbability **float64 `json:"epss_probability,omitempty" db:"epss_probability"`
|
||||
CISAKnownExploit **bool `json:"cisa_known_exploit,omitempty" db:"cisa_known_exploit"`
|
||||
CVEPublished **time.Time `json:"cve_published,omitempty" db:"cve_published"`
|
||||
Description **string `json:"cve_description,omitempty" db:"description"`
|
||||
ResolvedInVersion **string `json:"resolved_in_version,omitempty" db:"resolved_in_version"`
|
||||
}
|
||||
|
||||
type CVEMeta struct {
|
||||
@ -48,8 +49,9 @@ type SoftwareCPE struct {
|
||||
// SoftwareVulnerability is a vulnerability on a software.
|
||||
// Represents an entry in the `software_cve` table.
|
||||
type SoftwareVulnerability struct {
|
||||
SoftwareID uint `db:"software_id"`
|
||||
CVE string `db:"cve"`
|
||||
SoftwareID uint `db:"software_id"`
|
||||
CVE string `db:"cve"`
|
||||
ResolvedInVersion string `db:"resolved_in_version"`
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
|
@ -2912,8 +2912,9 @@ func (s *integrationEnterpriseTestSuite) TestListSoftware() {
|
||||
|
||||
inserted, err := s.ds.InsertSoftwareVulnerability(
|
||||
ctx, fleet.SoftwareVulnerability{
|
||||
SoftwareID: bar.ID,
|
||||
CVE: "cve-123",
|
||||
SoftwareID: bar.ID,
|
||||
CVE: "cve-123",
|
||||
ResolvedInVersion: "1.2.3",
|
||||
}, fleet.NVDSource,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@ -2955,6 +2956,7 @@ func (s *integrationEnterpriseTestSuite) TestListSoftware() {
|
||||
require.NotNil(t, barPayload.Vulnerabilities[0].CISAKnownExploit, ptr.BoolPtr(true))
|
||||
require.Equal(t, barPayload.Vulnerabilities[0].CVEPublished, ptr.TimePtr(now))
|
||||
require.Equal(t, barPayload.Vulnerabilities[0].Description, ptr.StringPtr("a long description of the cve"))
|
||||
require.Equal(t, barPayload.Vulnerabilities[0].ResolvedInVersion, ptr.StringPtr("1.2.3"))
|
||||
}
|
||||
|
||||
// TestGitOpsUserActions tests the permissions listed in ../../docs/Using-Fleet/Permissions.md.
|
||||
|
@ -9,17 +9,28 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/facebookincubator/nvdtools/cvefeed"
|
||||
feednvd "github.com/facebookincubator/nvdtools/cvefeed/nvd"
|
||||
"github.com/facebookincubator/nvdtools/cvefeed/nvd/schema"
|
||||
"github.com/facebookincubator/nvdtools/providers/nvd"
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
// Define a regex pattern for semver (simplified)
|
||||
var semverPattern = regexp.MustCompile(`^v?(\d+\.\d+\.\d+)`)
|
||||
|
||||
// Define a regex pattern for splitting version strings into subparts
|
||||
var nonNumericPartRegex = regexp.MustCompile(`(\d+)(\D.*)`)
|
||||
|
||||
// DownloadNVDCVEFeed downloads the NVD CVE feed. Skips downloading if the cve feed has not changed since the last time.
|
||||
func DownloadNVDCVEFeed(vulnPath string, cveFeedPrefixURL string) error {
|
||||
cve := nvd.SupportedCVE["cve-1.1.json.gz"]
|
||||
@ -110,11 +121,13 @@ func TranslateCPEToCVE(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get all the software CPEs from the database
|
||||
CPEs, err := ds.ListSoftwareCPEs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// hydrate the CPEs with the meta data
|
||||
var parsed []softwareCPEWithNVDMeta
|
||||
for _, CPE := range CPEs {
|
||||
attr, err := wfn.Parse(CPE.CPE)
|
||||
@ -243,9 +256,15 @@ func checkCVEs(
|
||||
}
|
||||
}
|
||||
|
||||
resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), softwareCPE.meta, dict, logger)
|
||||
if err != nil {
|
||||
level.Debug(logger).Log(logKey, "error", "err", err)
|
||||
}
|
||||
|
||||
vuln := fleet.SoftwareVulnerability{
|
||||
SoftwareID: softwareCPE.SoftwareID,
|
||||
CVE: matches.CVE.ID(),
|
||||
SoftwareID: softwareCPE.SoftwareID,
|
||||
CVE: matches.CVE.ID(),
|
||||
ResolvedInVersion: resolvedVersion,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
@ -272,3 +291,166 @@ func checkCVEs(
|
||||
|
||||
return foundVulns, nil
|
||||
}
|
||||
|
||||
// Returns the versionEndExcluding string for the given CVE and host software meta
|
||||
// data, if it exists in the NVD feed. This effectively gives us the version of the
|
||||
// software it needs to upgrade to in order to address the CVE.
|
||||
func getMatchingVersionEndExcluding(ctx context.Context, cve string, hostSoftwareMeta *wfn.Attributes, dict cvefeed.Dictionary, logger kitlog.Logger) (string, error) {
|
||||
vuln, ok := dict[cve].(*feednvd.Vuln)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Schema() maps to the JSON schema of the NVD feed for a given CVE
|
||||
vulnSchema := vuln.Schema()
|
||||
if vulnSchema == nil {
|
||||
level.Error(logger).Log("msg", "error getting schema for CVE", "cve", cve)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
config := vulnSchema.Configurations
|
||||
if config == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
nodes := config.Nodes
|
||||
if len(nodes) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
cpeMatch := findCPEMatch(nodes)
|
||||
if len(cpeMatch) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// convert the host software version to semver for later comparison
|
||||
formattedVersion := preprocessVersion(wfn.StripSlashes(hostSoftwareMeta.Version))
|
||||
softwareVersion, err := semver.NewVersion(formattedVersion)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "parsing software version", hostSoftwareMeta.Product, hostSoftwareMeta.Version)
|
||||
}
|
||||
|
||||
// Check if the host software version matches any of the CPEMatch rules.
|
||||
// CPEMatch rules can include version strings for the following:
|
||||
// - versionStartIncluding
|
||||
// - versionStartExcluding
|
||||
// - versionEndExcluding
|
||||
// - versionEndIncluding - not used in this function as we don't want to assume the resolved version
|
||||
for _, rule := range cpeMatch {
|
||||
if rule.VersionEndExcluding == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// convert the NVD cpe23URi to wfn.Attributes for later comparison
|
||||
attr, err := wfn.Parse(rule.Cpe23Uri)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "parsing cpe23Uri")
|
||||
}
|
||||
|
||||
// ensure the product and vendor match
|
||||
if attr.Product != hostSoftwareMeta.Product || attr.Vendor != hostSoftwareMeta.Vendor {
|
||||
continue
|
||||
}
|
||||
|
||||
// versionEnd is the version string that the vulnerable host software version must be less than
|
||||
versionEnd, err := checkVersion(ctx, rule, softwareVersion, cve)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "checking version")
|
||||
}
|
||||
if versionEnd != "" {
|
||||
return versionEnd, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// CPEMatch can be nested in Children nodes. Recursively search the nodes for a CPEMatch
|
||||
func findCPEMatch(nodes []*schema.NVDCVEFeedJSON10DefNode) []*schema.NVDCVEFeedJSON10DefCPEMatch {
|
||||
for _, node := range nodes {
|
||||
if len(node.CPEMatch) > 0 {
|
||||
return node.CPEMatch
|
||||
}
|
||||
|
||||
if len(node.Children) > 0 {
|
||||
match := findCPEMatch(node.Children)
|
||||
if match != nil {
|
||||
return match
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkVersion checks if the host software version matches the CPEMatch rule
|
||||
func checkVersion(ctx context.Context, rule *schema.NVDCVEFeedJSON10DefCPEMatch, softwareVersion *semver.Version, cve string) (string, error) {
|
||||
for _, condition := range []struct {
|
||||
startIncluding string
|
||||
startExcluding string
|
||||
}{
|
||||
{rule.VersionStartIncluding, ""},
|
||||
{"", rule.VersionStartExcluding},
|
||||
} {
|
||||
constraintStr := buildConstraintString(condition.startIncluding, condition.startExcluding, rule.VersionEndExcluding)
|
||||
if constraintStr == "" {
|
||||
return rule.VersionEndExcluding, nil
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(constraintStr)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrapf(ctx, err, "parsing constraint: %s for cve: %s", constraintStr, cve)
|
||||
}
|
||||
|
||||
if constraint.Check(softwareVersion) {
|
||||
return rule.VersionEndExcluding, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// buildConstraintString builds a semver constraint string from the startIncluding,
|
||||
// startExcluding, and endExcluding strings
|
||||
func buildConstraintString(startIncluding, startExcluding, endExcluding string) string {
|
||||
if startIncluding == "" && startExcluding == "" {
|
||||
return ""
|
||||
}
|
||||
startIncluding = preprocessVersion(startIncluding)
|
||||
startExcluding = preprocessVersion(startExcluding)
|
||||
endExcluding = preprocessVersion(endExcluding)
|
||||
|
||||
if startIncluding != "" {
|
||||
return fmt.Sprintf(">= %s, < %s", startIncluding, endExcluding)
|
||||
}
|
||||
return fmt.Sprintf("> %s, < %s", startExcluding, endExcluding)
|
||||
}
|
||||
|
||||
// Products using 4 part versioning scheme (ie. docker desktop)
|
||||
// need to be converted to 3 part versioning scheme (2.3.0.2 -> 2.3.0+3) for use with
|
||||
// the semver library.
|
||||
func preprocessVersion(version string) string {
|
||||
// If "+" is already present, validate the part before "+" as a semver
|
||||
if strings.Contains(version, "+") {
|
||||
parts := strings.Split(version, "+")
|
||||
if semverPattern.MatchString(parts[0]) {
|
||||
return version
|
||||
}
|
||||
}
|
||||
|
||||
// If the version string contains more than 3 parts, convert it to 3 parts
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) > 3 {
|
||||
return parts[0] + "." + parts[1] + "." + parts[2] + "+" + strings.Join(parts[3:], ".")
|
||||
}
|
||||
|
||||
// If the version string ends with a non-numeric character (like '1.0.0b'), replace
|
||||
// it with '+<char>' (like '1.0.0+b')
|
||||
if len(parts) == 3 {
|
||||
matches := nonNumericPartRegex.FindStringSubmatch(parts[2])
|
||||
if len(matches) > 2 {
|
||||
parts[2] = matches[1] + "+" + matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/facebookincubator/nvdtools/cvefeed"
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
"github.com/fleetdm/fleet/v4/pkg/nettest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
@ -337,3 +339,100 @@ func TestSyncsCVEFromURL(t *testing.T) {
|
||||
fmt.Sprintf("1 synchronisation error:\n\tunexpected size for \"%s/feeds/json/cve/1.1/nvdcve-1.1-2002.json.gz\" (200 OK): want 1453293, have 0", ts.URL),
|
||||
)
|
||||
}
|
||||
|
||||
// This test is using real data from the 2022 NVD feed
|
||||
func TestGetMatchingVersionEndExcluding(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testDict := loadDict(t, "../testdata/nvdcve-1.1-2022.json.gz")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cve string
|
||||
meta *wfn.Attributes
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "happy path with version with no Version Start",
|
||||
cve: "CVE-2022-40897",
|
||||
meta: &wfn.Attributes{
|
||||
Vendor: "python",
|
||||
Product: "setuptools",
|
||||
Version: "64",
|
||||
},
|
||||
want: "65.5.1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "CVE matches multiple products",
|
||||
cve: "CVE-2022-40956",
|
||||
meta: &wfn.Attributes{
|
||||
Vendor: "mozilla",
|
||||
Product: "firefox",
|
||||
Version: "93.0.100",
|
||||
},
|
||||
want: "105.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Nodes has nested Children",
|
||||
cve: "CVE-2022-40961",
|
||||
meta: &wfn.Attributes{
|
||||
Vendor: "mozilla",
|
||||
Product: "firefox",
|
||||
Version: "93.0.100",
|
||||
},
|
||||
want: "105.0",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getMatchingVersionEndExcluding(ctx, tt.cve, tt.meta, testDict, nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getMatchingVersionEndExcluding() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getMatchingVersionEndExcluding() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreprocessVersion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"2.3.0.2", "2.3.0+2"},
|
||||
{"2.3.0+2", "2.3.0+2"},
|
||||
{"v2.3.0+2", "v2.3.0+2"},
|
||||
{"2.3.0.2.5", "2.3.0+2.5"},
|
||||
{"2.3.0", "2.3.0"},
|
||||
{"2.3", "2.3"},
|
||||
{"v2.3.0", "v2.3.0"},
|
||||
{"notAVersion", "notAVersion"},
|
||||
{"2.0.0+svn315-7fakesync1ubuntu0.22.04.1", "2.0.0+svn315-7fakesync1ubuntu0.22.04.1"},
|
||||
{"1.21.1ubuntu2", "1.21.1+ubuntu2"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
output := preprocessVersion(tc.input)
|
||||
if output != tc.expected {
|
||||
t.Fatalf("expected: %s, got: %s", tc.expected, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// loadDict loads a cvefeed.Dictionary from a JSON NVD feed file.
|
||||
func loadDict(t *testing.T, path string) cvefeed.Dictionary {
|
||||
dict, err := cvefeed.LoadJSONDictionary(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user