diff --git a/syft/pkg/cataloger/cpe.go b/syft/pkg/cataloger/cpe.go index 346c446e6..1ba609536 100644 --- a/syft/pkg/cataloger/cpe.go +++ b/syft/pkg/cataloger/cpe.go @@ -1,16 +1,32 @@ package cataloger import ( + "bufio" + "bytes" "fmt" "net/url" "sort" "strings" + "github.com/scylladb/go-set/strset" + "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/pkg" "github.com/facebookincubator/nvdtools/wfn" ) +var domains = []string{ + "com", + "org", + "net", + "io", +} + +var ( + forbiddenProductGroupIDFields = strset.New("plugin", "plugins", "client") + forbiddenVendorGroupIDFields = strset.New("plugin", "plugins") +) + var productCandidatesByPkgType = candidateStore{ pkg.JavaPkg: { "springframework": []string{"spring_framework", "springsource_spring_framework"}, @@ -37,30 +53,6 @@ var productCandidatesByPkgType = candidateStore{ }, } -var cpeFilters = []filterFn{ - func(cpe pkg.CPE, p pkg.Package) bool { - // jira / atlassian should not apply to clients - if cpe.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") { - if cpe.Vendor == wfn.Any || cpe.Vendor == "jira" || cpe.Vendor == "atlassian" { - return true - } - } - return false - }, - // nolint: goconst - func(cpe pkg.CPE, p pkg.Package) bool { - // jenkins server should only match against a product with the name jenkins - if cpe.Product == "jenkins" && !strings.Contains(strings.ToLower(p.Name), "jenkins") { - if cpe.Vendor == wfn.Any || cpe.Vendor == "jenkins" || cpe.Vendor == "cloudbees" { - return true - } - } - return false - }, -} - -type filterFn func(cpe pkg.CPE, p pkg.Package) bool - // this is a static mapping of known package names (keys) to official cpe names for each package type candidateStore map[pkg.Type]map[string][]string @@ -87,7 +79,7 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes { return cpe } -func filterCpes(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) { +func filterCPEs(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) { cpeLoop: for _, cpe := range cpes { for _, fn := range filters { @@ -114,7 +106,7 @@ func generatePackageCPEs(p pkg.Package) []pkg.CPE { keys := internal.NewStringSet() cpes := make([]pkg.CPE, 0) for _, product := range products { - for _, vendor := range append([]string{wfn.Any}, vendors...) { + for _, vendor := range vendors { for _, targetSw := range append([]string{wfn.Any}, targetSws...) { // prevent duplicate entries... key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw) @@ -131,7 +123,7 @@ func generatePackageCPEs(p pkg.Package) []pkg.CPE { } // filter out any known combinations that don't accurately represent this package - cpes = filterCpes(cpes, p, cpeFilters...) + cpes = filterCPEs(cpes, p, cpeFilters...) sort.Sort(ByCPESpecificity(cpes)) @@ -168,52 +160,57 @@ func candidateTargetSoftwareAttrsForJava(p pkg.Package) []string { func candidateVendors(p pkg.Package) []string { // TODO: Confirm whether using products as vendors is helpful to the matching process - vendors := candidateProducts(p) + vendors := strset.New(candidateProducts(p)...) switch p.Language { + case pkg.Ruby: + vendors.Add("ruby-lang") case pkg.Java: if p.MetadataType == pkg.JavaMetadataType { - vendors = append(vendors, candidateVendorsForJava(p)...) + vendors.Add(candidateVendorsForJava(p)...) } case pkg.Go: // replace all candidates with only the golang-specific helper - vendors = nil + vendors.Clear() + vendor := candidateVendorForGo(p.Name) if vendor != "" { - vendors = []string{vendor} + vendors.Add(vendor) } } - return vendors + // try swapping hyphens for underscores, vice versa, and removing separators altogether + addSeparatorVariations(vendors) + + // generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci]) + return generateAllSubSelections(vendors.List()) } func candidateProducts(p pkg.Package) []string { - products := []string{p.Name} + products := strset.New(p.Name) - switch p.Language { - case pkg.Python: + switch { + case p.Language == pkg.Python: if !strings.HasPrefix(p.Name, "python") { - products = append(products, "python-"+p.Name) + products.Add("python-" + p.Name) } - case pkg.Java: - products = append(products, candidateProductsForJava(p)...) - case pkg.Go: + case p.Language == pkg.Java || p.MetadataType == pkg.JavaMetadataType: + products.Add(candidateProductsForJava(p)...) + case p.Language == pkg.Go: // replace all candidates with only the golang-specific helper - products = nil + products.Clear() + prod := candidateProductForGo(p.Name) if prod != "" { - products = []string{prod} + products.Add(prod) } } - for _, prod := range products { - if strings.Contains(prod, "-") { - products = append(products, strings.ReplaceAll(prod, "-", "_")) - } - } + // try swapping hyphens for underscores, vice versa, and removing separators altogether + addSeparatorVariations(products) - // return any known product name swaps prepended to the results - return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products...) + // prepend any known product name swaps prepended to the results + return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products.List()...) } // candidateProductForGo attempts to find a single product name in a best-effort attempt. This implementation prefers @@ -270,50 +267,78 @@ func candidateVendorForGo(name string) string { } func candidateProductsForJava(p pkg.Package) []string { - // TODO: we could get group-id-like info from the MANIFEST.MF "Automatic-Module-Name" field - // for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2 - // at /usr/share/jenkins/jenkins.war:WEB-INF/plugins/analysis-model-api.hpi:WEB-INF/lib/commons-io-2.8.0.jar - if product, _ := productAndVendorFromPomPropertiesGroupID(p); product != "" { - // ignore group ID info from a jenkins plugin, as using this info may imply that this package - // CPE belongs to the cloudbees org (or similar) which is wrong. - if p.Type == pkg.JenkinsPluginPkg && strings.ToLower(product) == "jenkins" { - return nil - } - return []string{product} - } - - return nil + return productsFromArtifactAndGroupIDs(artifactIDFromJavaPackage(p), groupIDsFromJavaPackage(p)) } func candidateVendorsForJava(p pkg.Package) []string { - if _, vendor := productAndVendorFromPomPropertiesGroupID(p); vendor != "" { - return []string{vendor} - } - - return nil + return vendorsFromGroupIDs(groupIDsFromJavaPackage(p)) } -func productAndVendorFromPomPropertiesGroupID(p pkg.Package) (string, string) { - groupID := groupIDFromPomProperties(p) - if !shouldConsiderGroupID(groupID) { - return "", "" +func vendorsFromGroupIDs(groupIDs []string) []string { + vendors := strset.New() + for _, groupID := range groupIDs { + for i, field := range strings.Split(groupID, ".") { + field = strings.TrimSpace(field) + + if len(field) == 0 { + continue + } + + if forbiddenVendorGroupIDFields.Has(strings.ToLower(field)) { + continue + } + + if i == 0 { + continue + } + + // e.g. jenkins-ci -> [jenkins-ci, jenkins] + vendors.Add(generateSubSelections(field)...) + } } - if !internal.HasAnyOfPrefixes(groupID, "com", "org") { - return "", "" - } - - fields := strings.Split(groupID, ".") - if len(fields) < 3 { - return "", "" - } - - product := fields[2] - vendor := fields[1] - return product, vendor + return vendors.List() } -func groupIDFromPomProperties(p pkg.Package) string { +func productsFromArtifactAndGroupIDs(artifactID string, groupIDs []string) []string { + products := strset.New() + if artifactID != "" { + products.Add(artifactID) + } + + for _, groupID := range groupIDs { + isPlugin := strings.Contains(artifactID, "plugin") || strings.Contains(groupID, "plugin") + + for i, field := range strings.Split(groupID, ".") { + field = strings.TrimSpace(field) + + if len(field) == 0 { + continue + } + + // don't add this field as a name if the name is implying the package is a plugin or client + if forbiddenProductGroupIDFields.Has(strings.ToLower(field)) { + continue + } + + if i <= 1 { + continue + } + + // umbrella projects tend to have sub components that either start or end with the project name. We want + // to identify fields that may represent the umbrella project, and not fields that indicate auxiliary + // information about the package. + couldBeProjectName := strings.HasPrefix(artifactID, field) || strings.HasSuffix(artifactID, field) + if artifactID == "" || (couldBeProjectName && !isPlugin) { + products.Add(field) + } + } + } + + return products.List() +} + +func artifactIDFromJavaPackage(p pkg.Package) string { metadata, ok := p.Metadata.(pkg.JavaMetadata) if !ok { return "" @@ -323,15 +348,156 @@ func groupIDFromPomProperties(p pkg.Package) string { return "" } - return metadata.PomProperties.GroupID + artifactID := strings.TrimSpace(metadata.PomProperties.ArtifactID) + if startsWithDomain(artifactID) && len(strings.Split(artifactID, ".")) > 1 { + // there is a strong indication that the artifact ID is really a group ID, don't use it + return "" + } + return artifactID } -func shouldConsiderGroupID(groupID string) bool { - if groupID == "" { - return false +func groupIDsFromJavaPackage(p pkg.Package) (groupIDs []string) { + metadata, ok := p.Metadata.(pkg.JavaMetadata) + if !ok { + return nil } - excludedGroupIDs := append([]string{pkg.JiraPluginPomPropertiesGroupID}, pkg.JenkinsPluginPomPropertiesGroupIDs...) + groupIDs = append(groupIDs, groupIDsFromPomProperties(metadata.PomProperties)...) + groupIDs = append(groupIDs, groupIDsFromJavaManifest(metadata.Manifest)...) - return !internal.HasAnyOfPrefixes(groupID, excludedGroupIDs...) + return groupIDs +} + +func groupIDsFromPomProperties(properties *pkg.PomProperties) (groupIDs []string) { + if properties == nil { + return nil + } + + if startsWithDomain(properties.GroupID) { + groupIDs = append(groupIDs, strings.TrimSpace(properties.GroupID)) + } + + // sometimes the publisher puts the group ID in the artifact ID field unintentionally + if startsWithDomain(properties.ArtifactID) && len(strings.Split(properties.ArtifactID, ".")) > 1 { + // there is a strong indication that the artifact ID is really a group ID + groupIDs = append(groupIDs, strings.TrimSpace(properties.ArtifactID)) + } + + return groupIDs +} + +func groupIDsFromJavaManifest(manifest *pkg.JavaManifest) (groupIDs []string) { + if manifest == nil { + return nil + } + // attempt to get group-id-like info from the MANIFEST.MF "Automatic-Module-Name" and "Extension-Name" field. + // for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2 + // at /usr/share/jenkins/jenkins.war:WEB-INF/plugins/analysis-model-api.hpi:WEB-INF/lib/commons-io-2.8.0.jar + // as well as the ant package from cloudbees/cloudbees-core-mm:2.277.2.4-ra. + for name, value := range manifest.Main { + value = strings.TrimSpace(value) + switch name { + case "Extension-Name", "Automatic-Module-Name": + if startsWithDomain(value) { + groupIDs = append(groupIDs, value) + } + } + } + for _, section := range manifest.NamedSections { + for name, value := range section { + value = strings.TrimSpace(value) + switch name { + case "Extension-Name", "Automatic-Module-Name": + if startsWithDomain(value) { + groupIDs = append(groupIDs, value) + } + } + } + } + return groupIDs +} + +func startsWithDomain(value string) bool { + return internal.HasAnyOfPrefixes(value, domains...) +} + +func generateAllSubSelections(fields []string) (results []string) { + for _, field := range fields { + results = append(results, generateSubSelections(field)...) + } + return results +} + +// generateSubSelections attempts to split a field by hyphens and underscores and return a list of sensible sub-selections +// that can be used as product or vendor candidates. E.g. jenkins-ci-tools -> [jenkins-ci-tools, jenkins-ci, jenkins]. +func generateSubSelections(field string) (results []string) { + scanner := bufio.NewScanner(strings.NewReader(field)) + scanner.Split(scanByHyphenOrUnderscore) + var lastToken uint8 + for scanner.Scan() { + rawCandidate := scanner.Text() + if len(rawCandidate) == 0 { + break + } + + // trim any number of hyphen or underscore that is prefixed/suffixed on the given candidate. Since + // scanByHyphenOrUnderscore preserves delimiters (hyphens and underscores) they are guaranteed to be at least + // prefixed. + candidate := strings.TrimFunc(rawCandidate, trimHyphenOrUnderscore) + + // capture the result (if there is content) + if len(candidate) > 0 { + if len(results) > 0 { + results = append(results, results[len(results)-1]+string(lastToken)+candidate) + } else { + results = append(results, candidate) + } + } + + // keep track of the trailing separator for the next loop + lastToken = rawCandidate[len(rawCandidate)-1] + } + return results +} + +// trimHyphenOrUnderscore is a character filter function for use with strings.TrimFunc in order to remove any hyphen or underscores. +func trimHyphenOrUnderscore(r rune) bool { + switch r { + case '-', '_': + return true + } + return false +} + +// scanByHyphenOrUnderscore splits on hyphen or underscore and includes the separator in the split +func scanByHyphenOrUnderscore(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexAny(data, "-_"); i >= 0 { + return i + 1, data[0 : i+1], nil + } + + if atEOF { + return len(data), data, nil + } + + return 0, nil, nil +} + +func addSeparatorVariations(fields *strset.Set) { + for _, field := range fields.List() { + hasHyphen := strings.Contains(field, "-") + hasUnderscore := strings.Contains(field, "_") + + if hasHyphen { + // provide variations of hyphen candidates with an underscore + fields.Add(strings.ReplaceAll(field, "-", "_")) + } + + if hasUnderscore { + // provide variations of underscore candidates with a hyphen + fields.Add(strings.ReplaceAll(field, "_", "-")) + } + } } diff --git a/syft/pkg/cataloger/cpe_filter.go b/syft/pkg/cataloger/cpe_filter.go new file mode 100644 index 000000000..9e06c0a7f --- /dev/null +++ b/syft/pkg/cataloger/cpe_filter.go @@ -0,0 +1,48 @@ +package cataloger + +import ( + "strings" + + "github.com/anchore/syft/syft/pkg" + "github.com/facebookincubator/nvdtools/wfn" +) + +const jenkinsName = "jenkins" + +type filterFn func(cpe pkg.CPE, p pkg.Package) bool + +var cpeFilters = []filterFn{ + jiraClientPackageFilter, + jenkinsPackageNameFilter, + jenkinsPluginFilter, +} + +// jenkins plugins should not match against jenkins +func jenkinsPluginFilter(cpe pkg.CPE, p pkg.Package) bool { + if p.Type == pkg.JenkinsPluginPkg && cpe.Product == jenkinsName { + return true + } + return false +} + +// filter to account that packages that are not for jenkins but have a CPE generated that will match against jenkins +func jenkinsPackageNameFilter(cpe pkg.CPE, p pkg.Package) bool { + // jenkins server should only match against a product with the name jenkins + if cpe.Product == jenkinsName && !strings.Contains(strings.ToLower(p.Name), jenkinsName) { + if cpe.Vendor == wfn.Any || cpe.Vendor == jenkinsName || cpe.Vendor == "cloudbees" { + return true + } + } + return false +} + +// filter to account for packages which are jira client packages but have a CPE that will match against jira +func jiraClientPackageFilter(cpe pkg.CPE, p pkg.Package) bool { + // jira / atlassian should not apply to clients + if cpe.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") { + if cpe.Vendor == wfn.Any || cpe.Vendor == "jira" || cpe.Vendor == "atlassian" { + return true + } + } + return false +} diff --git a/syft/pkg/cataloger/cpe_filter_test.go b/syft/pkg/cataloger/cpe_filter_test.go new file mode 100644 index 000000000..8e36eefdf --- /dev/null +++ b/syft/pkg/cataloger/cpe_filter_test.go @@ -0,0 +1,167 @@ +package cataloger + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_jenkinsPluginFilter(t *testing.T) { + tests := []struct { + name string + cpe pkg.CPE + pkg pkg.Package + expected bool + }{ + { + name: "go case (filter out)", + cpe: mustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Type: pkg.JenkinsPluginPkg, + }, + expected: true, + }, + { + name: "ignore jenkins plugins with unique name", + cpe: mustCPE("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Type: pkg.JenkinsPluginPkg, + }, + expected: false, + }, + { + name: "ignore java packages", + cpe: mustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Type: pkg.JavaPkg, + }, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, jenkinsPluginFilter(test.cpe, test.pkg)) + }) + } +} + +func Test_jenkinsPackageNameFilter(t *testing.T) { + tests := []struct { + name string + cpe pkg.CPE + pkg pkg.Package + expected bool + }{ + { + name: "filter out mismatched name (cloudbees vendor)", + cpe: mustCPE("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "not-j*nkins", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "filter out mismatched name (jenkins vendor)", + cpe: mustCPE("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "not-j*nkins", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "filter out mismatched name (any vendor)", + cpe: mustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "not-j*nkins", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "ignore packages with the name jenkins", + cpe: mustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "jenkins-thing", + Type: pkg.JavaPkg, + }, + expected: false, + }, + { + name: "ignore product names that are not exactly 'jenkins'", + cpe: mustCPE("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "not-j*nkins", + Type: pkg.JavaPkg, + }, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, jenkinsPackageNameFilter(test.cpe, test.pkg)) + }) + } +} + +func Test_jiraClientPackageFilter(t *testing.T) { + tests := []struct { + name string + cpe pkg.CPE + pkg pkg.Package + expected bool + }{ + { + name: "filter out mismatched name (atlassian vendor)", + cpe: mustCPE("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "something-client", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "filter out mismatched name (jira vendor)", + cpe: mustCPE("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "something-client", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "filter out mismatched name (any vendor)", + cpe: mustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "something-client", + Type: pkg.JavaPkg, + }, + expected: true, + }, + { + name: "ignore package names that do not have 'client'", + cpe: mustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "jira-thing", + Type: pkg.JavaPkg, + }, + expected: false, + }, + { + name: "ignore product names that are not exactly 'jira'", + cpe: mustCPE("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"), + pkg: pkg.Package{ + Name: "not-j*ra", + Type: pkg.JavaPkg, + }, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, jiraClientPackageFilter(test.cpe, test.pkg)) + }) + } +} diff --git a/syft/pkg/cataloger/cpe_specificity.go b/syft/pkg/cataloger/cpe_specificity.go index 1e927704b..179f8ce85 100644 --- a/syft/pkg/cataloger/cpe_specificity.go +++ b/syft/pkg/cataloger/cpe_specificity.go @@ -1,31 +1,49 @@ package cataloger -import "github.com/facebookincubator/nvdtools/wfn" +import ( + "sort" + + "github.com/facebookincubator/nvdtools/wfn" +) + +var _ sort.Interface = (*ByCPESpecificity)(nil) type ByCPESpecificity []wfn.Attributes -// Implementing sort.Interface -func (c ByCPESpecificity) Len() int { return len(c) } +func (c ByCPESpecificity) Len() int { return len(c) } + func (c ByCPESpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + func (c ByCPESpecificity) Less(i, j int) bool { - return countSpecifiedFields(c[i]) > countSpecifiedFields(c[j]) + iScore := weightedCountForSpecifiedFields(c[i]) + jScore := weightedCountForSpecifiedFields(c[j]) + + if iScore == jScore { + return countFieldLength(c[i]) > countFieldLength(c[j]) + } + return iScore > jScore } -func countSpecifiedFields(cpe wfn.Attributes) int { - checksForSpecifiedField := []func(cpe wfn.Attributes) bool{ - func(cpe wfn.Attributes) bool { return cpe.Part != "" }, - func(cpe wfn.Attributes) bool { return cpe.Product != "" }, - func(cpe wfn.Attributes) bool { return cpe.Vendor != "" }, - func(cpe wfn.Attributes) bool { return cpe.Version != "" }, - func(cpe wfn.Attributes) bool { return cpe.TargetSW != "" }, +func countFieldLength(cpe wfn.Attributes) int { + return len(cpe.Part + cpe.Vendor + cpe.Product + cpe.Version + cpe.TargetSW) +} + +func weightedCountForSpecifiedFields(cpe wfn.Attributes) int { + checksForSpecifiedField := []func(cpe wfn.Attributes) (bool, int){ + func(cpe wfn.Attributes) (bool, int) { return cpe.Part != "", 2 }, + func(cpe wfn.Attributes) (bool, int) { return cpe.Vendor != "", 3 }, + func(cpe wfn.Attributes) (bool, int) { return cpe.Product != "", 4 }, + func(cpe wfn.Attributes) (bool, int) { return cpe.Version != "", 1 }, + func(cpe wfn.Attributes) (bool, int) { return cpe.TargetSW != "", 1 }, } - count := 0 + weightedCount := 0 for _, fieldIsSpecified := range checksForSpecifiedField { - if fieldIsSpecified(cpe) { - count++ + isSpecified, weight := fieldIsSpecified(cpe) + if isSpecified { + weightedCount += weight } } - return count + return weightedCount } diff --git a/syft/pkg/cataloger/cpe_specificity_test.go b/syft/pkg/cataloger/cpe_specificity_test.go new file mode 100644 index 000000000..3bd4787cc --- /dev/null +++ b/syft/pkg/cataloger/cpe_specificity_test.go @@ -0,0 +1,92 @@ +package cataloger + +import ( + "sort" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func mustCPE(c string) pkg.CPE { + return must(pkg.NewCPE(c)) +} + +func must(c pkg.CPE, e error) pkg.CPE { + if e != nil { + panic(e) + } + return c +} + +func TestCPESpecificity(t *testing.T) { + tests := []struct { + name string + input []pkg.CPE + expected []pkg.CPE + }{ + { + name: "sort strictly by wfn *", + input: []pkg.CPE{ + mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + }, + expected: []pkg.CPE{ + mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + }, + }, + { + name: "sort strictly by field length", + input: []pkg.CPE{ + mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + }, + expected: []pkg.CPE{ + mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + }, + }, + { + name: "sort by mix of field length and specificity", + input: []pkg.CPE{ + mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + }, + expected: []pkg.CPE{ + mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sort.Sort(ByCPESpecificity(test.input)) + assert.Equal(t, test.expected, test.input) + }) + } + +} diff --git a/syft/pkg/cataloger/cpe_test.go b/syft/pkg/cataloger/cpe_test.go index c122084db..7c5a16801 100644 --- a/syft/pkg/cataloger/cpe_test.go +++ b/syft/pkg/cataloger/cpe_test.go @@ -3,6 +3,7 @@ package cataloger import ( "fmt" "sort" + "strings" "testing" "github.com/anchore/syft/syft/pkg" @@ -27,14 +28,10 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.DebPkg, }, expected: []string{ - "cpe:2.3:a:*:name-part:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name-part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name-part:name-part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name-part:name-part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python-name-part:name-part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python-name-part:name-part:3.2:*:*:*:*:python:*:*", - "cpe:2.3:a:*:name_part:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name_part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name_part:name_part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name_part:name_part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python_name_part:name_part:3.2:*:*:*:*:*:*:*", @@ -47,10 +44,6 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:python-name-part:name_part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python_name_part:name-part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python_name_part:name-part:3.2:*:*:*:*:python:*:*", - "cpe:2.3:a:*:python-name-part:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:python-name-part:3.2:*:*:*:*:python:*:*", - "cpe:2.3:a:*:python_name_part:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:python_name_part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name-part:python-name-part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name-part:python-name-part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name-part:python_name_part:3.2:*:*:*:*:*:*:*", @@ -67,6 +60,38 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:python_name_part:python-name-part:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python_name_part:python_name_part:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python_name_part:python_name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:name:name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:name:name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:name:name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:name:name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:name:python-name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:name:python-name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:name:python_name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:name:python_name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python-name:name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python-name:name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python-name:name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python-name:name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python-name:python-name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python-name:python-name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python-name:python_name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python-name:python_name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:python-name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python-name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:python_name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python_name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python_name:name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python_name:name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python_name:name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python_name:name_part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python_name:python-name-part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python_name:python-name-part:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python_name:python_name_part:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python_name:python_name_part:3.2:*:*:*:*:python:*:*", }, }, { @@ -79,18 +104,12 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.DebPkg, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python-name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python-name:name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python_name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python_name:name:3.2:*:*:*:*:python:*:*", - "cpe:2.3:a:*:python-name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:python-name:3.2:*:*:*:*:python:*:*", - "cpe:2.3:a:*:python_name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:python_name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name:python-name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:python-name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:name:python_name:3.2:*:*:*:*:*:*:*", @@ -103,6 +122,12 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:python_name:python-name:3.2:*:*:*:*:python:*:*", "cpe:2.3:a:python_name:python_name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:python_name:python_name:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:name:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:python-name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python-name:3.2:*:*:*:*:python:*:*", + "cpe:2.3:a:python:python_name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:python:python_name:3.2:*:*:*:*:python:*:*", }, }, { @@ -115,9 +140,6 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.DebPkg, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:node.js:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:nodejs:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:node.js:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:nodejs:*:*", @@ -133,12 +155,18 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.DebPkg, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:ruby:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:rails:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:ruby:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:rails:*:*", + "cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:rails:*:*", + "cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:ruby:*:*", + "cpe:2.3:a:ruby:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:name:3.2:*:*:*:*:rails:*:*", + "cpe:2.3:a:ruby:name:3.2:*:*:*:*:ruby:*:*", + "cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:rails:*:*", + "cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:ruby:*:*", }, }, { @@ -151,9 +179,6 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.DebPkg, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*", @@ -175,18 +200,12 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*", "cpe:2.3:a:sonatype:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:sonatype:name:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:sonatype:name:3.2:*:*:*:*:maven:*:*", - "cpe:2.3:a:*:nexus:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:nexus:3.2:*:*:*:*:java:*:*", - "cpe:2.3:a:*:nexus:3.2:*:*:*:*:maven:*:*", "cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:maven:*:*", @@ -211,9 +230,6 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.JenkinsPluginPkg, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", @@ -234,12 +250,12 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:jenkins:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jenkins:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:jenkins:name:3.2:*:*:*:*:jenkins:*:*", }, }, { @@ -257,12 +273,18 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", + "cpe:2.3:a:name:something:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:name:something:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:name:something:3.2:*:*:*:*:jenkins:*:*", + "cpe:2.3:a:something:name:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:something:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:something:name:3.2:*:*:*:*:jenkins:*:*", + "cpe:2.3:a:something:something:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:something:something:3.2:*:*:*:*:cloudbees_jenkins:*:*", + "cpe:2.3:a:something:something:3.2:*:*:*:*:jenkins:*:*", }, }, { @@ -280,9 +302,6 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", @@ -303,9 +322,6 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", @@ -326,9 +342,6 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*", - "cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", @@ -351,9 +364,6 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:java:*:*", - "cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:maven:*:*", "cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:maven:*:*", @@ -366,6 +376,42 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:java:*:*", "cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:java:*:*", + "cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:maven:*:*", }, }, { @@ -385,21 +431,12 @@ func TestGeneratePackageCPEs(t *testing.T) { }, }, expected: []string{ - "cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", - "cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", - "cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", - "cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", - "cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", - "cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", - "cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:*:*:*", - "cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:java:*:*", - "cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:maven:*:*", "cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", @@ -412,15 +449,30 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", - "cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:*:*:*", - "cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:java:*:*", - "cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:maven:*:*", "cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", "cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", "cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", "cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*", + "cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*", + "cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*", + "cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", }, }, { @@ -433,9 +485,6 @@ func TestGeneratePackageCPEs(t *testing.T) { Type: pkg.GoModulePkg, }, expected: []string{ - "cpe:2.3:a:*:something:3.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:something:3.2:*:*:*:*:go:*:*", - "cpe:2.3:a:*:something:3.2:*:*:*:*:golang:*:*", "cpe:2.3:a:someone:something:3.2:*:*:*:*:*:*:*", "cpe:2.3:a:someone:something:3.2:*:*:*:*:go:*:*", "cpe:2.3:a:someone:something:3.2:*:*:*:*:golang:*:*", @@ -464,16 +513,22 @@ func TestGeneratePackageCPEs(t *testing.T) { actualCpeSet.Add(a.BindToFmtString()) } - extra := strset.Difference(actualCpeSet, expectedCpeSet).List() + extra := strset.Difference(expectedCpeSet, actualCpeSet).List() sort.Strings(extra) + if len(extra) > 0 { + t.Errorf("found extra CPEs:") + } for _, d := range extra { - t.Errorf("extra CPE: %+v", d) + fmt.Printf(" %q,\n", d) } - missing := strset.Difference(expectedCpeSet, actualCpeSet).List() + missing := strset.Difference(actualCpeSet, expectedCpeSet).List() sort.Strings(missing) + if len(missing) > 0 { + t.Errorf("missing CPEs:") + } for _, d := range missing { - t.Errorf("missing CPE: %+v", d) + fmt.Printf(" %q,\n", d) } }) } @@ -515,7 +570,7 @@ func TestCandidateProducts(t *testing.T) { }, }, }, - expected: []string{"some-jenkins-plugin", "some_jenkins_plugin"}, + expected: []string{"some-jenkins-plugin", "some_jenkins_plugin", "jenkins"}, }, { p: pkg.Package{ @@ -698,3 +753,379 @@ func TestCandidateVendorForGo(t *testing.T) { }) } } + +func Test_generateSubSelections(t *testing.T) { + tests := []struct { + field string + expected []string + }{ + { + field: "jenkins", + expected: []string{"jenkins"}, + }, + { + field: "jenkins-ci", + expected: []string{"jenkins", "jenkins-ci"}, + }, + { + field: "jenkins--ci", + expected: []string{"jenkins", "jenkins-ci"}, + }, + { + field: "jenkins_ci_tools", + expected: []string{"jenkins", "jenkins_ci", "jenkins_ci_tools"}, + }, + { + field: "-jenkins", + expected: []string{"jenkins"}, + }, + { + field: "jenkins_", + expected: []string{"jenkins"}, + }, + { + field: "", + expected: nil, + }, + { + field: "-", + expected: nil, + }, + { + field: "_", + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.field, func(t *testing.T) { + assert.ElementsMatch(t, test.expected, generateSubSelections(test.field)) + }) + } +} + +func Test_addSeparatorVariations(t *testing.T) { + tests := []struct { + input []string + expected []string + }{ + { + input: []string{"jenkins-ci"}, + expected: []string{"jenkins-ci", "jenkins_ci"}, //, "jenkinsci"}, + }, + { + input: []string{"jenkins_ci"}, + expected: []string{"jenkins_ci", "jenkins-ci"}, //, "jenkinsci"}, + }, + { + input: []string{"jenkins"}, + expected: []string{"jenkins"}, + }, + { + input: []string{"jenkins-ci", "circle-ci"}, + expected: []string{"jenkins-ci", "jenkins_ci", "circle-ci", "circle_ci"}, //, "jenkinsci", "circleci"}, + }, + } + for _, test := range tests { + t.Run(strings.Join(test.input, ","), func(t *testing.T) { + val := strset.New(test.input...) + addSeparatorVariations(val) + assert.ElementsMatch(t, test.expected, val.List()) + }) + } +} + +func Test_productsFromArtifactAndGroupIDs(t *testing.T) { + tests := []struct { + groupIDs []string + artifactID string + expected []string + }{ + { + groupIDs: []string{"org.sonatype.nexus"}, + artifactID: "nexus-extender", + expected: []string{"nexus", "nexus-extender"}, + }, + { + groupIDs: []string{"org.sonatype.nexus"}, + expected: []string{"nexus"}, + }, + { + groupIDs: []string{"org.jenkins-ci.plugins"}, + artifactID: "ant", + expected: []string{"ant"}, + }, + { + groupIDs: []string{"org.jenkins-ci.plugins"}, + artifactID: "antisamy-markup-formatter", + expected: []string{"antisamy-markup-formatter"}, + }, + { + groupIDs: []string{"io.jenkins.plugins"}, + artifactID: "aws-global-configuration", + expected: []string{"aws-global-configuration"}, + }, + { + groupIDs: []string{"com.cloudbees.jenkins.plugins"}, + artifactID: "cloudbees-servicenow-jenkins-plugin", + expected: []string{"cloudbees-servicenow-jenkins-plugin"}, + }, + { + groupIDs: []string{"com.atlassian.confluence.plugins"}, + artifactID: "confluence-mobile-plugin", + expected: []string{"confluence-mobile-plugin"}, + }, + { + groupIDs: []string{"com.atlassian.confluence.plugins"}, + artifactID: "confluence-view-file-macro", + expected: []string{"confluence-view-file-macro"}, + }, + { + groupIDs: []string{"com.google.guava"}, + artifactID: "failureaccess", + expected: []string{"failureaccess"}, + }, + } + for _, test := range tests { + t.Run(strings.Join(test.groupIDs, ",")+":"+test.artifactID, func(t *testing.T) { + actual := productsFromArtifactAndGroupIDs(test.artifactID, test.groupIDs) + assert.ElementsMatch(t, test.expected, actual, "different products") + }) + } +} + +func Test_candidateProductsForJava(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected []string + }{ + { + name: "duplicate groupID in artifactID field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + GroupID: "org.sonatype.nexus", + ArtifactID: "org.sonatype.nexus", + }, + }, + }, + expected: []string{"nexus"}, + }, + { + name: "detect groupID-like value in artifactID field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + ArtifactID: "org.sonatype.nexus", + }, + }, + }, + expected: []string{"nexus"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := candidateProductsForJava(test.pkg) + assert.ElementsMatch(t, test.expected, actual, "different products") + }) + } +} + +func Test_vendorsFromGroupIDs(t *testing.T) { + tests := []struct { + groupIDs []string + expected []string + }{ + { + groupIDs: []string{"org.sonatype.nexus"}, + expected: []string{"sonatype", "nexus"}, + }, + { + groupIDs: []string{"org.sonatype.nexus"}, + expected: []string{"sonatype", "nexus"}, + }, + { + groupIDs: []string{"org.sonatype.nexus"}, + expected: []string{"sonatype", "nexus"}, + }, + { + groupIDs: []string{"org.jenkins-ci.plugins"}, + expected: []string{"jenkins-ci", "jenkins"}, + }, + { + groupIDs: []string{"org.jenkins-ci.plugins"}, + expected: []string{"jenkins-ci", "jenkins"}, + }, + { + groupIDs: []string{"io.jenkins.plugins"}, + expected: []string{"jenkins"}, + }, + { + groupIDs: []string{"com.cloudbees.jenkins.plugins"}, + expected: []string{"cloudbees", "jenkins"}, + }, + { + groupIDs: []string{"com.atlassian.confluence.plugins"}, + expected: []string{"atlassian", "confluence"}, + }, + { + groupIDs: []string{"com.atlassian.confluence.plugins"}, + expected: []string{"atlassian", "confluence"}, + }, + { + groupIDs: []string{"com.google.guava"}, + expected: []string{"google", "guava"}, + }, + } + for _, test := range tests { + t.Run(strings.Join(test.groupIDs, ","), func(t *testing.T) { + actual := vendorsFromGroupIDs(test.groupIDs) + assert.ElementsMatch(t, test.expected, actual, "different vendors") + }) + } +} + +func Test_groupIDsFromJavaPackage(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expects []string + }{ + { + name: "go case", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + GroupID: "io.jenkins-ci.plugin.thing", + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "from artifactID", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + ArtifactID: "io.jenkins-ci.plugin.thing", + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "from main Extension-Name field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Extension-Name": "io.jenkins-ci.plugin.thing", + }, + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "from named section Extension-Name field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + Manifest: &pkg.JavaManifest{ + NamedSections: map[string]map[string]string{ + "section": { + "Extension-Name": "io.jenkins-ci.plugin.thing", + }, + }, + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "from main Automatic-Module-Name field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + Manifest: &pkg.JavaManifest{ + Main: map[string]string{ + "Automatic-Module-Name": "io.jenkins-ci.plugin.thing", + }, + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "from named section Automatic-Module-Name field", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + Manifest: &pkg.JavaManifest{ + NamedSections: map[string]map[string]string{ + "section": { + "Automatic-Module-Name": "io.jenkins-ci.plugin.thing", + }, + }, + }, + }, + }, + expects: []string{"io.jenkins-ci.plugin.thing"}, + }, + { + name: "no manifest or pom info", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{}, + }, + expects: nil, + }, + { + name: "no java info", + pkg: pkg.Package{}, + expects: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expects, groupIDsFromJavaPackage(test.pkg)) + }) + } +} + +func Test_artifactIDFromJavaPackage(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expects string + }{ + { + name: "go case", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + ArtifactID: "cloudbees-installation-manager", + }, + }, + }, + expects: "cloudbees-installation-manager", + }, + { + name: "ignore groupID-like things", + pkg: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + ArtifactID: "io.jenkins-ci.plugin.thing", + }, + }, + }, + expects: "", + }, + { + name: "no java info", + pkg: pkg.Package{}, + expects: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expects, artifactIDFromJavaPackage(test.pkg)) + }) + } +} diff --git a/syft/pkg/java_metadata.go b/syft/pkg/java_metadata.go index 3036418a3..f54d9af31 100644 --- a/syft/pkg/java_metadata.go +++ b/syft/pkg/java_metadata.go @@ -7,8 +7,6 @@ import ( "github.com/package-url/packageurl-go" ) -const JiraPluginPomPropertiesGroupID = "com.atlassian.jira.plugins" - var JenkinsPluginPomPropertiesGroupIDs = []string{ "io.jenkins.plugins", "org.jenkins.plugins",