Add version_resolved_in to software API (#13939)

This commit is contained in:
Tim Lee 2023-09-18 16:53:32 -06:00 committed by GitHub
parent c508209e11
commit 338c64d78b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 519 additions and 50 deletions

View File

@ -0,0 +1 @@
- (premium only) adds `resolved_in_version` to `/fleet/software` APIs pulled from NVD feed

View File

@ -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
}

View File

@ -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

View File

@ -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),

View File

@ -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) {

View File

@ -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.

View File

@ -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.

View File

@ -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, ".")
}

View File

@ -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
}