Enhance CPE generation (#472)

* adjust CPE specificity sorting to include field length and bias certain fields

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove * vendor values from CPE generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* re-enable generating CPEs for jenkins and jira plugins

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* improve CPE generation logic based on java artifactID and groupID

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add ruby-lang as target software candidate for gems in CPE generation logic

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* rename filterCpes to filterCPEs

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* refactor CPE filters and groupID processing (for linting)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use ruby-lang as vendor candidate not target software

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* address PR comments for CPE generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2021-08-09 15:52:19 -04:00 committed by GitHub
parent 3a5168917e
commit 98d4749f86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1097 additions and 177 deletions

View File

@ -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 !internal.HasAnyOfPrefixes(groupID, "com", "org") {
return "", ""
if forbiddenVendorGroupIDFields.Has(strings.ToLower(field)) {
continue
}
fields := strings.Split(groupID, ".")
if len(fields) < 3 {
return "", ""
if i == 0 {
continue
}
product := fields[2]
vendor := fields[1]
return product, vendor
// e.g. jenkins-ci -> [jenkins-ci, jenkins]
vendors.Add(generateSubSelections(field)...)
}
}
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, "_", "-"))
}
}
}

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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) 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
}

View File

@ -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)
})
}
}

View File

@ -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))
})
}
}

View File

@ -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",