diff --git a/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go b/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go index d177827d6be..812bd09a71c 100644 --- a/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go +++ b/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go @@ -11,6 +11,12 @@ type candidateComposite struct { candidateAddition } +type candidateRemovalComposite struct { + pkg.Type + candidateKey + candidateRemovals +} + // defaultCandidateAdditions is all of the known cases for product and vendor field values that should be used when // select package information is discovered var defaultCandidateAdditions = buildCandidateLookup( @@ -123,6 +129,16 @@ var defaultCandidateAdditions = buildCandidateLookup( }, }) +var defaultCandidateRemovals = buildCandidateRemovalLookup( + []candidateRemovalComposite{ + // Python packages + { + pkg.PythonPkg, + candidateKey{PkgName: "redis"}, + candidateRemovals{VendorsToRemove: []string{"redis"}}, + }, + }) + // buildCandidateLookup is a convenience function for creating the defaultCandidateAdditions set func buildCandidateLookup(cc []candidateComposite) (ca map[pkg.Type]map[candidateKey]candidateAddition) { ca = make(map[pkg.Type]map[candidateKey]candidateAddition) @@ -136,12 +152,30 @@ func buildCandidateLookup(cc []candidateComposite) (ca map[pkg.Type]map[candidat return ca } +// buildCandidateRemovalLookup is a convenience function for creating the defaultCandidateRemovals set +func buildCandidateRemovalLookup(cc []candidateRemovalComposite) (ca map[pkg.Type]map[candidateKey]candidateRemovals) { + ca = make(map[pkg.Type]map[candidateKey]candidateRemovals) + for _, c := range cc { + if _, ok := ca[c.Type]; !ok { + ca[c.Type] = make(map[candidateKey]candidateRemovals) + } + ca[c.Type][c.candidateKey] = c.candidateRemovals + } + return ca +} + // candidateKey represents the set of inputs that should be matched on in order to signal more candidate additions to be used. type candidateKey struct { Vendor string PkgName string } +// candidateRemovals are the specific removals that should be considered during CPE generation (given a specific candidateKey) +type candidateRemovals struct { + ProductsToRemove []string + VendorsToRemove []string +} + // candidateAddition are the specific additions that should be considered during CPE generation (given a specific candidateKey) type candidateAddition struct { AdditionalProducts []string @@ -192,3 +226,35 @@ func findAdditionalProducts(allAdditions map[pkg.Type]map[candidateKey]candidate return products } + +// findVendorsToRemove searches all possible vendor removals that could be removed during the CPE generation process (given package info + a vendor candidate) +func findVendorsToRemove(allRemovals map[pkg.Type]map[candidateKey]candidateRemovals, ty pkg.Type, pkgName string) (vendors []string) { + removals, ok := allRemovals[ty] + if !ok { + return nil + } + + if removal, ok := removals[candidateKey{ + PkgName: pkgName, + }]; ok { + vendors = append(vendors, removal.VendorsToRemove...) + } + + return vendors +} + +// findProductsToRemove searches all possible product removals that could be removed during the CPE generation process (given package info) +func findProductsToRemove(allRemovals map[pkg.Type]map[candidateKey]candidateRemovals, ty pkg.Type, pkgName string) (products []string) { + removals, ok := allRemovals[ty] + if !ok { + return nil + } + + if removal, ok := removals[candidateKey{ + PkgName: pkgName, + }]; ok { + products = append(products, removal.ProductsToRemove...) + } + + return products +} diff --git a/syft/pkg/cataloger/common/cpe/candidate_by_package_type_test.go b/syft/pkg/cataloger/common/cpe/candidate_by_package_type_test.go index 90e2c1cdc58..471b45cb973 100644 --- a/syft/pkg/cataloger/common/cpe/candidate_by_package_type_test.go +++ b/syft/pkg/cataloger/common/cpe/candidate_by_package_type_test.go @@ -154,3 +154,81 @@ func Test_additionalVendors(t *testing.T) { }) } } + +func Test_findVendorsToRemove(t *testing.T) { + //GIVEN + tests := []struct { + name string + ty pkg.Type + pkgName string + expected []string + }{ + { + name: "vendor removal match by input package name", + ty: pkg.JavaPkg, + pkgName: "my-package-name", + expected: []string{"awesome-vendor-addition"}, + }, + { + name: "vendor removal miss by input package name", + ty: pkg.JavaPkg, + pkgName: "my-package-name-1", + }, + } + + allRemovals := map[pkg.Type]map[candidateKey]candidateRemovals{ + pkg.JavaPkg: { + candidateKey{ + PkgName: "my-package-name", + }: { + VendorsToRemove: []string{"awesome-vendor-addition"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + //WHEN + THEN + assert.Equal(t, test.expected, findVendorsToRemove(allRemovals, test.ty, test.pkgName)) + }) + } +} + +func Test_findProductsToRemove(t *testing.T) { + //GIVEN + tests := []struct { + name string + ty pkg.Type + pkgName string + expected []string + }{ + { + name: "vendor removal match by input package name", + ty: pkg.JavaPkg, + pkgName: "my-package-name", + expected: []string{"awesome-vendor-addition"}, + }, + { + name: "vendor removal miss by input package name", + ty: pkg.JavaPkg, + pkgName: "my-package-name-1", + }, + } + + allRemovals := map[pkg.Type]map[candidateKey]candidateRemovals{ + pkg.JavaPkg: { + candidateKey{ + PkgName: "my-package-name", + }: { + ProductsToRemove: []string{"awesome-vendor-addition"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + //WHEN + THEN + assert.Equal(t, test.expected, findProductsToRemove(allRemovals, test.ty, test.pkgName)) + }) + } +} diff --git a/syft/pkg/cataloger/common/cpe/generate.go b/syft/pkg/cataloger/common/cpe/generate.go index afce232dae4..a48e5784a8a 100644 --- a/syft/pkg/cataloger/common/cpe/generate.go +++ b/syft/pkg/cataloger/common/cpe/generate.go @@ -116,6 +116,9 @@ func candidateVendors(p pkg.Package) []string { vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, p.Type, p.Name, vendor)...) } + // remove known mis + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, p.Type, p.Name)...) + return vendors.uniqueValues() } @@ -148,6 +151,9 @@ func candidateProducts(p pkg.Package) []string { // add known candidate additions products.addValue(findAdditionalProducts(defaultCandidateAdditions, p.Type, p.Name)...) + // remove known candidate removals + products.removeByValue(findProductsToRemove(defaultCandidateRemovals, p.Type, p.Name)...) + return products.uniqueValues() } diff --git a/syft/pkg/cataloger/common/cpe/generate_test.go b/syft/pkg/cataloger/common/cpe/generate_test.go index 518ed6f26ec..187302b9dd8 100644 --- a/syft/pkg/cataloger/common/cpe/generate_test.go +++ b/syft/pkg/cataloger/common/cpe/generate_test.go @@ -666,6 +666,27 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:stephanie_morillo:bundler:2.1.4:*:*:*:*:*:*:*", }, }, + { + name: "regression: python redis shadows normal redis", + p: pkg.Package{ + Name: "redis", + Version: "2.1.4", + Type: pkg.PythonPkg, + FoundBy: "some-analyzer", + Language: pkg.Python, + }, + expected: []string{ + "cpe:2.3:a:python-redis:python-redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python-redis:python_redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python-redis:redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python-redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python_redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python:redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python_redis:python-redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python_redis:python_redis:2.1.4:*:*:*:*:*:*:*", + "cpe:2.3:a:python_redis:redis:2.1.4:*:*:*:*:*:*:*", + }, + }, } for _, test := range tests {