From 0bda69227407eaf1ea3a35f1119b5980f27f4e38 Mon Sep 17 00:00:00 2001 From: Weston Steimel Date: Fri, 29 May 2026 16:35:27 +0100 Subject: [PATCH] fix: improve platform CPE determination logic Ensures that CPEs are only surfaced as platforms if the versionless CPE is marked as non-vulnerable across all elements within a candidate node. This prevents unaffected ranges of a package from being marked as a platform of itself. Signed-off-by: Weston Steimel --- grype/db/v6/build/transformers/nvd/node.go | 22 ++++- ...d-and-unaffected-range-with-platforms.json | 68 ++++++++++++++ .../build/transformers/nvd/transform_test.go | 90 +++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 grype/db/v6/build/transformers/nvd/testdata/product-affected-and-unaffected-range-with-platforms.json diff --git a/grype/db/v6/build/transformers/nvd/node.go b/grype/db/v6/build/transformers/nvd/node.go index 1a4f4b9fa3e..8ee1fa0a4bf 100644 --- a/grype/db/v6/build/transformers/nvd/node.go +++ b/grype/db/v6/build/transformers/nvd/node.go @@ -8,6 +8,7 @@ import ( "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" + "github.com/scylladb/go-set/strset" ) type affectedPackageCandidate struct { @@ -250,9 +251,12 @@ func extractVulnerableCPEs(node nvd.Node, cfg Config) ([]affectedPackageCandidat // extractPlatformCPEs extracts all platform CPEs from a node (explicitly non-vulnerable CPEs). Why not just // use the part indication (i.e. 'h' & 'o' are platform and 'a' is the vulnerable candidate)? Because you can -// find cases where an application is the platform (e.g. kubernetes or openshift). +// find cases where an application is the platform (e.g. kubernetes or openshift). Any platform CPE candidate +// must be non-vulnerable across all cpeMatch elements of the node, otherwise unaffected package entries will be +// mistakenly raised up as platforms func extractPlatformCPEs(node nvd.Node) ([]cpe.Attributes, error) { - var platformCPEs []cpe.Attributes + platformCPEMap := make(map[string][]cpe.Attributes) + packageCandidates := strset.New() for _, match := range node.CpeMatch { cpeAttr, err := cpe.NewAttributes(match.Criteria) @@ -260,11 +264,21 @@ func extractPlatformCPEs(node nvd.Node) ([]cpe.Attributes, error) { return nil, fmt.Errorf("unable to parse CPE '%s': %w", match.Criteria, err) } - if !match.Vulnerable { - platformCPEs = append(platformCPEs, cpeAttr) + key := cpeKey(cpeAttr) + if match.Vulnerable { + packageCandidates.Add(key) + delete(platformCPEMap, key) + } else if !packageCandidates.Has(key) { + platformCPEMap[key] = append(platformCPEMap[key], cpeAttr) } } + var platformCPEs []cpe.Attributes + + for _, platforms := range platformCPEMap { + platformCPEs = append(platformCPEs, platforms...) + } + return platformCPEs, nil } diff --git a/grype/db/v6/build/transformers/nvd/testdata/product-affected-and-unaffected-range-with-platforms.json b/grype/db/v6/build/transformers/nvd/testdata/product-affected-and-unaffected-range-with-platforms.json new file mode 100644 index 00000000000..a2a13c98f5a --- /dev/null +++ b/grype/db/v6/build/transformers/nvd/testdata/product-affected-and-unaffected-range-with-platforms.json @@ -0,0 +1,68 @@ +{ + "cve": { + "id": "CVE-2025-30331111111", + "sourceIdentifier": "security@mozilla.org", + "published": "2025-04-01T13:15:41.697", + "lastModified": "2026-04-13T15:16:57.157", + "vulnStatus": "Modified", + "cveTags": [], + "descriptions": [ + { + "lang": "en", + "value": "" + } + ], + "configurations": [ + { + "nodes": [ + { + "cpeMatch": [ + { + "criteria": "cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*", + "matchCriteriaId": "21CE0201-DC5C-5924-95C1-B46F85CE3F2D", + "versionEndExcluding": "137", + "vulnerable": true, + "fix": { + "version": "137", + "date": "2025-09-04", + "kind": "first-observed" + } + }, + { + "criteria": "cpe:2.3:a:mozilla:firefox_esr:*:*:*:*:*:*:*:*", + "matchCriteriaId": "0C620750-B827-56C5-A96A-9AEC0EE2F3AD", + "versionEndExcluding": "137", + "vulnerable": true, + "fix": { + "version": "137", + "date": "2025-09-04", + "kind": "first-observed" + } + }, + { + "criteria": "cpe:2.3:a:mozilla:firefox:108:*:*:*:*:*:*:*", + "matchCriteriaId": "0C620750-B827-56C5-A96A-9AEC0EE2F3AD", + "vulnerable": false + } + ], + "negate": false, + "operator": "OR" + }, + { + "cpeMatch": [ + { + "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", + "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA", + "vulnerable": false + } + ], + "negate": false, + "operator": "OR" + } + ], + "operator": "AND" + } + ], + "references": [] + } +} \ No newline at end of file diff --git a/grype/db/v6/build/transformers/nvd/transform_test.go b/grype/db/v6/build/transformers/nvd/transform_test.go index 625c378ba9c..c8e2bc3cb3f 100644 --- a/grype/db/v6/build/transformers/nvd/transform_test.go +++ b/grype/db/v6/build/transformers/nvd/transform_test.go @@ -2161,6 +2161,96 @@ func TestTransform(t *testing.T) { }, }, }, + { + name: "Determine platform CPEs from node with both affected and unaffected product ranges", + fixture: "testdata/product-affected-and-unaffected-range-with-platforms.json", + provider: "nvd", + config: defaultConfig(), + want: []transformers.RelatedEntries{ + { + VulnerabilityHandle: &db.VulnerabilityHandle{ + Name: "CVE-2025-30331111111", + ProviderID: "nvd", + Provider: expectedProvider("nvd"), + ModifiedDate: timeRef(time.Date(2026, 4, 13, 15, 16, 57, 157000000, time.UTC)), + PublishedDate: timeRef(time.Date(2025, 4, 1, 13, 15, 41, 697000000, time.UTC)), + Status: db.VulnerabilityActive, + BlobValue: &db.VulnerabilityBlob{ + ID: "CVE-2025-30331111111", + Assigners: []string{"security@mozilla.org"}, + Description: "", + References: []db.Reference{ + { + URL: "https://nvd.nist.gov/vuln/detail/CVE-2025-30331111111", + }, + }, + }, + }, + Related: relatedEntries( + db.AffectedCPEHandle{ + BlobValue: &db.PackageBlob{ + CVEs: []string{"CVE-2025-30331111111"}, + Qualifiers: &db.PackageQualifiers{ + PlatformCPEs: []string{"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"}, + }, + Ranges: []db.Range{ + { + Version: db.Version{ + Constraint: "< 137", + }, + Fix: &db.Fix{ + Version: "137", + State: db.FixedStatus, + Detail: &v6.FixDetail{ + Available: &v6.FixAvailability{ + Date: timeRef(time.Date(2025, 9, 4, 0, 0, 0, 0, time.UTC)), + Kind: "first-observed", + }, + }, + }, + }, + }, + }, + CPE: &db.Cpe{ + Part: "a", + Vendor: "mozilla", + Product: "firefox", + }, + }, + db.AffectedCPEHandle{ + BlobValue: &db.PackageBlob{ + CVEs: []string{"CVE-2025-30331111111"}, + Qualifiers: &db.PackageQualifiers{ + PlatformCPEs: []string{"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"}, + }, + Ranges: []db.Range{ + { + Version: db.Version{ + Constraint: "< 137", + }, + Fix: &db.Fix{ + Version: "137", + State: db.FixedStatus, + Detail: &v6.FixDetail{ + Available: &v6.FixAvailability{ + Date: timeRef(time.Date(2025, 9, 4, 0, 0, 0, 0, time.UTC)), + Kind: "first-observed", + }, + }, + }, + }, + }, + }, + CPE: &db.Cpe{ + Part: "a", + Vendor: "mozilla", + Product: "firefox_esr", + }, + }, + ), + }, + }, + }, } for _, test := range tests {