diff --git a/syft/pkg/cataloger/cpe.go b/syft/pkg/cataloger/cpe.go index a972cf535..b53bdfb99 100644 --- a/syft/pkg/cataloger/cpe.go +++ b/syft/pkg/cataloger/cpe.go @@ -54,6 +54,9 @@ func (s candidateStore) getCandidates(t pkg.Type, key string) []string { return value } +const pomPropertiesGroupIDJenkinsPlugins = "com.cloudbees.jenkins.plugins" +const pomPropertiesGroupIDJiraPlugins = "com.atlassian.jira.plugins" + func newCPE(product, vendor, version, targetSW string) wfn.Attributes { cpe := *(wfn.NewAttributesWithAny()) cpe.Part = "a" @@ -100,7 +103,7 @@ func candidateTargetSoftwareAttrs(p pkg.Package) []string { var targetSw []string switch p.Language { case pkg.Java: - targetSw = append(targetSw, "java", "maven") + targetSw = append(targetSw, candidateTargetSoftwareAttrsForJava(p)...) case pkg.JavaScript: targetSw = append(targetSw, "node.js", "nodejs") case pkg.Ruby: @@ -109,51 +112,130 @@ func candidateTargetSoftwareAttrs(p pkg.Package) []string { targetSw = append(targetSw, "python") } - if p.Type == pkg.JenkinsPluginPkg { - targetSw = append(targetSw, "jenkins", "cloudbees_jenkins") - } - return targetSw } +func isJenkinsPlugin(p pkg.Package) bool { + if p.Type == pkg.JenkinsPluginPkg { + return true + } + + if groupID := groupIDFromPomProperties(p); groupID == pomPropertiesGroupIDJenkinsPlugins { + return true + } + + return false +} + +func candidateTargetSoftwareAttrsForJava(p pkg.Package) []string { + // Use the more specific indicator if available + + if isJenkinsPlugin(p) { + return []string{"jenkins", "cloudbees_jenkins"} + } + + return []string{"java", "maven"} +} + func candidateVendors(p pkg.Package) []string { + // TODO: Confirm whether using products as vendors is helpful to the matching process vendors := candidateProducts(p) + switch p.Language { case pkg.Python: vendors = append(vendors, fmt.Sprintf("python-%s", p.Name)) case pkg.Java: if p.MetadataType == pkg.JavaMetadataType { - if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil { - // derive the vendor from the groupID (e.g. org.sonatype.nexus --> sonatype) - if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") { - fields := strings.Split(metadata.PomProperties.GroupID, ".") - if len(fields) >= 3 { - vendors = append(vendors, fields[1]) - } - } - } + vendors = append(vendors, candidateVendorsForJava(p)...) } } return vendors } func candidateProducts(p pkg.Package) []string { - var products = []string{p.Name} - + products := []string{p.Name} if p.Language == pkg.Java { - if p.MetadataType == pkg.JavaMetadataType { - if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil { - // derive the product from the groupID (e.g. org.sonatype.nexus --> nexus) - if strings.HasPrefix(metadata.PomProperties.GroupID, "org.") || strings.HasPrefix(metadata.PomProperties.GroupID, "com.") { - fields := strings.Split(metadata.PomProperties.GroupID, ".") - if len(fields) >= 3 { - products = append(products, fields[2]) - } - } - } - } + products = append(products, candidateProductsForJava(p)...) } // return any known product name swaps prepended to the results return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products...) } + +func candidateProductsForJava(p pkg.Package) []string { + if product, _ := productAndVendorFromPomPropertiesGroupID(p); product != "" { + return []string{product} + } + + return nil +} + +func candidateVendorsForJava(p pkg.Package) []string { + if _, vendor := productAndVendorFromPomPropertiesGroupID(p); vendor != "" { + return []string{vendor} + } + + return nil +} + +func productAndVendorFromPomPropertiesGroupID(p pkg.Package) (string, string) { + groupID := groupIDFromPomProperties(p) + if !shouldConsiderGroupID(groupID) { + return "", "" + } + + if !hasAnyOfPrefixes(groupID, "com", "org") { + return "", "" + } + + fields := strings.Split(groupID, ".") + if len(fields) < 3 { + return "", "" + } + + product := fields[2] + vendor := fields[1] + return product, vendor +} + +func groupIDFromPomProperties(p pkg.Package) string { + metadata, ok := p.Metadata.(pkg.JavaMetadata) + if !ok { + return "" + } + + if metadata.PomProperties == nil { + return "" + } + + return metadata.PomProperties.GroupID +} + +func shouldConsiderGroupID(groupID string) bool { + if groupID == "" { + return false + } + + excludedGroupIDs := []string{ + pomPropertiesGroupIDJiraPlugins, + pomPropertiesGroupIDJenkinsPlugins, + } + + for _, excludedGroupID := range excludedGroupIDs { + if groupID == excludedGroupID { + return false + } + } + + return true +} + +func hasAnyOfPrefixes(input string, prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(input, prefix) { + return true + } + } + + return false +} diff --git a/syft/pkg/cataloger/cpe_test.go b/syft/pkg/cataloger/cpe_test.go index e5a0be66c..560c574b0 100644 --- a/syft/pkg/cataloger/cpe_test.go +++ b/syft/pkg/cataloger/cpe_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGenerate(t *testing.T) { +func TestGeneratePackageCPEs(t *testing.T) { tests := []struct { name string p pkg.Package @@ -132,7 +132,7 @@ func TestGenerate(t *testing.T) { }, }, { - name: "jenkins package", + name: "jenkins package identified via pkg type", p: pkg.Package{ Name: "name", Version: "3.2", @@ -142,13 +142,32 @@ func TestGenerate(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: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:*:*:*:*:java:*:*", - "cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*", + "cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*", + "cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", + }, + }, + { + name: "jenkins package identified via groupId", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.Java, + Type: pkg.JavaPkg, + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + GroupID: "com.cloudbees.jenkins.plugins", + }, + }, + }, + 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:*:*", }, @@ -165,13 +184,13 @@ func TestGenerate(t *testing.T) { actualCpeSet.Add(a.BindToFmtString()) } - extra := strset.Difference(expectedCpeSet, actualCpeSet).List() + extra := strset.Difference(actualCpeSet, expectedCpeSet).List() sort.Strings(extra) for _, d := range extra { t.Errorf("extra CPE: %+v", d) } - missing := strset.Difference(actualCpeSet, expectedCpeSet).List() + missing := strset.Difference(expectedCpeSet, actualCpeSet).List() sort.Strings(missing) for _, d := range missing { t.Errorf("missing CPE: %+v", d)