diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go index c5e29a89761..0f91871dc72 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go +++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_for_pe.go @@ -6,14 +6,20 @@ import ( "github.com/anchore/syft/syft/pkg" ) -// candidateVendorsForPE returns vendor candidates for PE (BinaryPkg) packages based on common metadata hints. -// Specifically, normalize Ghostscript binaries to vendor "artifex" when detected. -func candidateVendorsForPE(p pkg.Package) fieldCandidateSet { - candidates := newFieldCandidateSet() +// isMicrosoftEdgePE returns true if the PE metadata resembles a Microsoft Edge binary (e.g. msedge.exe). +func isMicrosoftEdgePE(product, fileDesc string) bool { + return product == "microsoft edge" || fileDesc == "microsoft edge" +} +// candidateVendorsForPE adds vendor candidates for PE (BinaryPkg) packages based on common metadata hints, +// mutating the given set in place. Specifically, normalize Ghostscript binaries to vendor "artifex" when detected. +// Unlike sibling candidateXForY helpers in this package, this mutates the caller's set directly (rather than +// returning a new one to union in) because the Microsoft Edge case below needs to clear() the caller's existing +// (incorrect) candidates, not just add to them. +func candidateVendorsForPE(p pkg.Package, vendors fieldCandidateSet) { meta, ok := p.Metadata.(pkg.PEBinary) if !ok { - return candidates + return } var company, product, fileDesc string @@ -29,25 +35,30 @@ func candidateVendorsForPE(p pkg.Package) fieldCandidateSet { } if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") || strings.Contains(company, "artifex") { - candidates.addValue("artifex") + vendors.addValue("artifex") } if product == "git" || fileDesc == "git setup" || company == "the git development community" { - candidates.addValue("git_for_windows_project") - candidates.addValue("gitforwindows") + vendors.addValue("git_for_windows_project") + vendors.addValue("gitforwindows") } - return candidates + if isMicrosoftEdgePE(product, fileDesc) { + // the raised package name ("Microsoft Edge") is otherwise used verbatim as the vendor guess, which + // does not match the NVD CPE dictionary entry (vendor: microsoft); replace it outright so the + // mismatched candidate doesn't win on specificity and get selected as the primary CPE. Delimiter + // variations are disallowed so this exact, known-correct value isn't out-ranked by a hyphenated form. + vendors.clear() + vendors.add(fieldCandidate{value: "microsoft", disallowSubSelections: true, disallowDelimiterVariations: true}) + } } -// candidateProductsForPE returns product candidates for PE (BinaryPkg) packages based on common metadata hints. -// Specifically, normalize Ghostscript binaries to product "ghostscript" when detected. -func candidateProductsForPE(p pkg.Package) fieldCandidateSet { - candidates := newFieldCandidateSet() - +// candidateProductsForPE adds product candidates for PE (BinaryPkg) packages based on common metadata hints, +// mutating the given set in place. Specifically, normalize Ghostscript binaries to product "ghostscript" when detected. +func candidateProductsForPE(p pkg.Package, products fieldCandidateSet) { meta, ok := p.Metadata.(pkg.PEBinary) if !ok { - return candidates + return } var product, fileDesc string @@ -61,13 +72,20 @@ func candidateProductsForPE(p pkg.Package) fieldCandidateSet { } if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") { - candidates.addValue("ghostscript") + products.addValue("ghostscript") } if product == "git" || fileDesc == "git setup" { - candidates.addValue("git_for_windows") - candidates.addValue("git") + products.addValue("git_for_windows") + products.addValue("git") } - return candidates + if isMicrosoftEdgePE(product, fileDesc) { + // the raised package name ("Microsoft Edge") is otherwise used verbatim as the product guess, which + // does not match the NVD CPE dictionary entry (product: edge_chromium); replace it outright so the + // mismatched candidate doesn't win on specificity and get selected as the primary CPE. Delimiter + // variations are disallowed so this exact, known-correct value isn't out-ranked by a hyphenated form. + products.clear() + products.add(fieldCandidate{value: "edge_chromium", disallowSubSelections: true, disallowDelimiterVariations: true}) + } } diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate.go b/syft/pkg/cataloger/internal/cpegenerate/generate.go index 097cf4e329a..1b37074cd05 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate.go @@ -283,7 +283,7 @@ func candidateVendorsByType(p pkg.Package, vendors fieldCandidateSet) fieldCandi vendors.union(candidateVendorsForJavascript(p)) case pkg.PEBinary: // Add PE-specific vendor hints (e.g. ghostscript -> artifex) - vendors.union(candidateVendorsForPE(p)) + candidateVendorsForPE(p, vendors) case pkg.WordpressPluginEntry: vendors.clear() vendors.union(candidateVendorsForWordpressPlugin(p)) @@ -328,7 +328,7 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet { products.union(candidateProductsForAPK(p)) case pkg.PEBinary: // Add PE-specific product hints (e.g. ghostscript) - products.union(candidateProductsForPE(p)) + candidateProductsForPE(p, products) case pkg.WordpressPluginEntry: products.clear() products.union(candidateProductsForWordpressPlugin(p)) diff --git a/syft/pkg/cataloger/internal/cpegenerate/pe_test.go b/syft/pkg/cataloger/internal/cpegenerate/pe_test.go index b8bd78ea7e7..d09e87db831 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/pe_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/pe_test.go @@ -37,3 +37,37 @@ func TestGhostscriptPEGeneratesArtifexCPE(t *testing.T) { t.Fatalf("expected to find CPE with vendor 'artifex' and product 'ghostscript' for Ghostscript PE binary; got: %+v", cpes) } } + +func TestMicrosoftEdgePEGeneratesCorrectCPE(t *testing.T) { + // construct a BinaryPkg with PE metadata resembling Microsoft Edge (e.g. msedge.exe) + p := pkg.Package{ + Name: "Microsoft Edge", + Version: "122.0.2365.106", + Type: pkg.BinaryPkg, + Metadata: pkg.PEBinary{ + VersionResources: pkg.KeyValues{ + {Key: "CompanyName", Value: "Microsoft Corporation"}, + {Key: "ProductName", Value: "Microsoft Edge"}, + {Key: "FileDescription", Value: "Microsoft Edge"}, + }, + }, + } + + cpes := FromPackageAttributes(p) + if len(cpes) == 0 { + t.Fatalf("expected at least one CPE, got none") + } + + // the first CPE is the one selected as the package's primary CPE in output formats (e.g. CycloneDX), + // so it must be the correct vendor/product pairing and not the raw "Microsoft_Edge:Microsoft_Edge" guess. + got := cpes[0].Attributes + if got.Vendor != "microsoft" || got.Product != "edge_chromium" || got.Version != p.Version { + t.Fatalf("expected primary CPE vendor 'microsoft' and product 'edge_chromium' for Microsoft Edge PE binary; got: %+v", cpes) + } + + for _, c := range cpes { + if c.Attributes.Vendor == "microsoft_edge" || c.Attributes.Product == "microsoft_edge" { + t.Fatalf("did not expect a CPE derived verbatim from the raised package name; got: %+v", cpes) + } + } +}