mirror of
https://github.com/anchore/syft.git
synced 2026-11-06 01:16:32 +01:00
Internalize CPE generation logic (#2541)
* migrate CPE generation logic to internal Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove create function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
prefixesToPackageType = map[string]pkg.Type{
|
||||
"py-": pkg.PythonPkg,
|
||||
"ruby-": pkg.GemPkg,
|
||||
}
|
||||
streamVersionPkgNamePattern = regexp.MustCompile(`^(?P<stream>[a-zA-Z][\w-]*?)(?P<streamVersion>\-?\d[\d\.]*?)($|-(?P<subPackage>[a-zA-Z][\w-]*?)?)$`)
|
||||
)
|
||||
|
||||
type upstreamCandidate struct {
|
||||
Name string
|
||||
Type pkg.Type
|
||||
}
|
||||
|
||||
func upstreamCandidates(m pkg.ApkDBEntry) (candidates []upstreamCandidate) {
|
||||
// Do not consider OriginPackage variations when generating CPE candidates for the child package
|
||||
// because doing so will result in false positives when matching to vulnerabilities in Grype since
|
||||
// it won't know to lookup apk fix entries using the OriginPackage name.
|
||||
|
||||
name := m.Package
|
||||
groups := internal.MatchNamedCaptureGroups(streamVersionPkgNamePattern, m.Package)
|
||||
stream, ok := groups["stream"]
|
||||
|
||||
if ok && stream != "" {
|
||||
sub, ok := groups["subPackage"]
|
||||
|
||||
if ok && sub != "" {
|
||||
name = fmt.Sprintf("%s-%s", stream, sub)
|
||||
} else {
|
||||
name = stream
|
||||
}
|
||||
}
|
||||
|
||||
for prefix, typ := range prefixesToPackageType {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
t := strings.TrimPrefix(name, prefix)
|
||||
if t != "" {
|
||||
candidates = append(candidates, upstreamCandidate{Name: t, Type: typ})
|
||||
return candidates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
candidates = append(candidates, upstreamCandidate{Name: name, Type: pkg.UnknownPkg})
|
||||
return candidates
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func candidateVendorsForAPK(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.ApkDBEntry)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
candidates := upstreamCandidates(metadata)
|
||||
|
||||
for _, c := range candidates {
|
||||
switch c.Type {
|
||||
case pkg.UnknownPkg:
|
||||
vendors.addValue(c.Name)
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.ApkPkg, c.Name, c.Name)...)
|
||||
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.ApkPkg, c.Name)...)
|
||||
case pkg.PythonPkg:
|
||||
vendors.addValue(c.Name)
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, c.Type, c.Name, c.Name)...)
|
||||
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, c.Type, c.Name)...)
|
||||
for _, av := range additionalVendorsForPython(c.Name) {
|
||||
vendors.addValue(av)
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, av, av)...)
|
||||
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, av)...)
|
||||
}
|
||||
default:
|
||||
vendors.addValue(c.Name)
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, c.Type, c.Name, c.Name)...)
|
||||
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, c.Type, c.Name)...)
|
||||
}
|
||||
}
|
||||
|
||||
vendors.union(candidateVendorsFromURL(metadata.URL))
|
||||
|
||||
for v := range vendors {
|
||||
v.disallowDelimiterVariations = true
|
||||
v.disallowSubSelections = true
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func candidateProductsForAPK(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.ApkDBEntry)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
products := newFieldCandidateSet()
|
||||
candidates := upstreamCandidates(metadata)
|
||||
|
||||
for _, c := range candidates {
|
||||
switch c.Type {
|
||||
case pkg.UnknownPkg:
|
||||
products.addValue(c.Name)
|
||||
products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.ApkPkg, c.Name)...)
|
||||
products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.ApkPkg, c.Name)...)
|
||||
default:
|
||||
products.addValue(c.Name)
|
||||
products.addValue(findAdditionalProducts(defaultCandidateAdditions, c.Type, c.Name)...)
|
||||
products.removeByValue(findProductsToRemove(defaultCandidateRemovals, c.Type, c.Name)...)
|
||||
}
|
||||
}
|
||||
|
||||
for p := range products {
|
||||
p.disallowDelimiterVariations = true
|
||||
p.disallowSubSelections = true
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_candidateVendorsForAPK(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "py3-cryptography Package",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "py3-cryptography",
|
||||
},
|
||||
},
|
||||
expected: []string{"python-cryptography_project", "cryptography", "cryptographyproject", "cryptography_project"},
|
||||
},
|
||||
{
|
||||
name: "py2-pypdf with explicit different origin",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "py2-pypdf",
|
||||
OriginPackage: "abcdefg",
|
||||
},
|
||||
},
|
||||
expected: []string{"pypdf", "pypdfproject", "pypdf_project"},
|
||||
},
|
||||
{
|
||||
name: "ruby-armadillo Package",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-armadillo",
|
||||
},
|
||||
},
|
||||
expected: []string{"armadillo"},
|
||||
},
|
||||
{
|
||||
name: "python-3.6",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "python-3.6",
|
||||
},
|
||||
},
|
||||
expected: []string{"python", "python_software_foundation"},
|
||||
},
|
||||
{
|
||||
name: "ruby-3.6",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-3.6",
|
||||
URL: "https://www.ruby-lang.org/",
|
||||
},
|
||||
},
|
||||
expected: []string{"ruby", "ruby-lang"},
|
||||
},
|
||||
{
|
||||
name: "make",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "make",
|
||||
URL: "https://www.gnu.org/software/make",
|
||||
},
|
||||
},
|
||||
expected: []string{"gnu", "make"},
|
||||
},
|
||||
{
|
||||
name: "ruby-rake with matching origin",
|
||||
pkg: pkg.Package{
|
||||
Name: "ruby-rake",
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-rake",
|
||||
URL: "https://github.com/ruby/rake",
|
||||
OriginPackage: "ruby-rake",
|
||||
},
|
||||
},
|
||||
expected: []string{"rake", "ruby-lang", "ruby"},
|
||||
},
|
||||
{
|
||||
name: "ruby-rake with non-matching origin",
|
||||
pkg: pkg.Package{
|
||||
Name: "ruby-rake",
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-rake",
|
||||
URL: "https://www.ruby-lang.org/",
|
||||
OriginPackage: "ruby",
|
||||
},
|
||||
},
|
||||
expected: []string{"rake", "ruby-lang"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, candidateVendorsForAPK(test.pkg).uniqueValues(), "different vendors")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_candidateProductsForAPK(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "py3-cryptography Package",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "py3-cryptography",
|
||||
},
|
||||
},
|
||||
expected: []string{"cryptography", "python-cryptography"},
|
||||
},
|
||||
{
|
||||
name: "py2-pypdf with explicit different origin",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "py2-pypdf",
|
||||
OriginPackage: "abcdefg",
|
||||
},
|
||||
},
|
||||
expected: []string{"pypdf"},
|
||||
},
|
||||
{
|
||||
name: "ruby-armadillo Package",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-armadillo",
|
||||
},
|
||||
},
|
||||
expected: []string{"armadillo"},
|
||||
},
|
||||
{
|
||||
name: "python-3.6",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "python-3.6",
|
||||
},
|
||||
},
|
||||
expected: []string{"python"},
|
||||
},
|
||||
{
|
||||
name: "ruby-3.6",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-3.6",
|
||||
URL: "https://www.ruby-lang.org/",
|
||||
},
|
||||
},
|
||||
expected: []string{"ruby"},
|
||||
},
|
||||
{
|
||||
name: "make",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "make",
|
||||
URL: "https://www.gnu.org/software/make",
|
||||
},
|
||||
},
|
||||
expected: []string{"make"},
|
||||
},
|
||||
{
|
||||
name: "ruby-rake with matching origin",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-rake",
|
||||
URL: "https://github.com/ruby/rake",
|
||||
OriginPackage: "ruby-rake",
|
||||
},
|
||||
},
|
||||
expected: []string{"rake"},
|
||||
},
|
||||
{
|
||||
name: "ruby-rake with non-matching origin",
|
||||
pkg: pkg.Package{
|
||||
Name: "ruby-rake",
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-rake",
|
||||
URL: "https://www.ruby-lang.org/",
|
||||
OriginPackage: "ruby",
|
||||
},
|
||||
},
|
||||
expected: []string{"rake"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, candidateProductsForAPK(test.pkg).uniqueValues(), "different products")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_upstreamCandidates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata pkg.ApkDBEntry
|
||||
expected []upstreamCandidate
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "p",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "p", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same package and origin simple case",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "p",
|
||||
OriginPackage: "p",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "p", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different package and origin",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "p",
|
||||
OriginPackage: "origin",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "p", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream python package information as qualifier py- prefix",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "py-potatoes",
|
||||
OriginPackage: "py-potatoes",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "potatoes", Type: pkg.PythonPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream python package information as qualifier py3- prefix",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "py3-potatoes",
|
||||
OriginPackage: "py3-potatoes",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "potatoes", Type: pkg.PythonPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "python package with distinct origin package",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "py3-non-existant",
|
||||
OriginPackage: "abcdefg",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "non-existant", Type: pkg.PythonPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream ruby package information as qualifier",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-something",
|
||||
OriginPackage: "ruby-something",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "something", Type: pkg.GemPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby package with distinct origin package",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-something",
|
||||
OriginPackage: "1234567",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "something", Type: pkg.GemPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "postgesql-15 upstream postgresql",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "postgresql-15",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "postgresql", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "postgesql15 upstream postgresql",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "postgresql15",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "postgresql", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "go-1.19 upstream go",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "go-1.19",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "go", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "go1.143 upstream go",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "go1.143",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "go", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abc-101.191.23456 upstream abc",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "abc-101.191.23456",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "abc", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abc101.191.23456 upstream abc",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "abc101.191.23456",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "abc", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abc101-12345-1045 upstream abc101-12345",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "abc101-12345-1045",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "abc101-12345", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abc101-a12345-1045 upstream abc101-a12345",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "abc101-a12345-1045",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "abc-a12345-1045", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "package starting with single digit",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "3proxy",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "3proxy", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "package starting with multiple digits",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "356proxy",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "356proxy", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "package composed of only digits",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "123456",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "123456", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby-3.6 upstream ruby",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-3.6",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "ruby", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby3.6 upstream ruby",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby3.6",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "ruby", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby3.6-tacos upstream tacos",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby3.6-tacos",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "tacos", Type: pkg.GemPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ruby-3.6-tacos upstream tacos",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "ruby-3.6-tacos",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "tacos", Type: pkg.GemPkg},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abc1234jksajflksa",
|
||||
metadata: pkg.ApkDBEntry{
|
||||
Package: "abc1234jksajflksa",
|
||||
},
|
||||
expected: []upstreamCandidate{
|
||||
{Name: "abc1234jksajflksa", Type: pkg.UnknownPkg},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := upstreamCandidates(test.metadata)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,605 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// candidateComposite is a convenience when creating the defaultCandidateAdditions set
|
||||
type candidateComposite struct {
|
||||
pkg.Type
|
||||
candidateKey
|
||||
candidateAddition
|
||||
}
|
||||
|
||||
type candidateRemovalComposite struct {
|
||||
pkg.Type
|
||||
candidateKey
|
||||
candidateRemovals
|
||||
}
|
||||
|
||||
// defaultCandidateAdditions is all of the known cases for product and vendor field values that should be used when
|
||||
// select package information is discovered
|
||||
var defaultCandidateAdditions = buildCandidateLookup(
|
||||
[]candidateComposite{
|
||||
// Java packages
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "springframework"},
|
||||
candidateAddition{AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"}, AdditionalVendors: []string{"pivotal_software", "springsource", "vmware"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "spring-core"},
|
||||
candidateAddition{AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"}, AdditionalVendors: []string{"pivotal_software", "springsource", "vmware"}},
|
||||
},
|
||||
{
|
||||
// example image: docker.io/jenkins/jenkins:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "spring-security-core"},
|
||||
candidateAddition{AdditionalProducts: []string{"spring_security"}, AdditionalVendors: []string{"vmware"}},
|
||||
},
|
||||
{
|
||||
// example image: docker.io/nuxeo:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "elasticsearch"}, // , Vendor: "elasticsearch"},
|
||||
candidateAddition{AdditionalVendors: []string{"elastic"}},
|
||||
},
|
||||
{
|
||||
// example image: docker.io/kaazing-gateway:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "log4j"}, // , Vendor: "apache-software-foundation"},
|
||||
candidateAddition{AdditionalVendors: []string{"apache"}},
|
||||
},
|
||||
|
||||
{
|
||||
// example image: cassandra:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "apache-cassandra"}, // , Vendor: "apache"},
|
||||
candidateAddition{AdditionalProducts: []string{"cassandra"}},
|
||||
},
|
||||
{
|
||||
// example image: cloudbees/cloudbees-core-mm:2.319.3.4
|
||||
// this is a wrapped packaging of the handlebars.js node module
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "handlebars"},
|
||||
candidateAddition{AdditionalVendors: []string{"handlebarsjs"}},
|
||||
},
|
||||
// NPM packages
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "hapi"},
|
||||
candidateAddition{AdditionalProducts: []string{"hapi_server_framework"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "handlebars.js"},
|
||||
candidateAddition{AdditionalProducts: []string{"handlebars"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "is-my-json-valid"},
|
||||
candidateAddition{AdditionalProducts: []string{"is_my_json_valid"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "mustache"},
|
||||
candidateAddition{AdditionalProducts: []string{"mustache.js"}},
|
||||
},
|
||||
|
||||
// Gem packages
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "Arabic-Prawn"},
|
||||
candidateAddition{AdditionalProducts: []string{"arabic_prawn"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "bio-basespace-sdk"},
|
||||
candidateAddition{AdditionalProducts: []string{"basespace_ruby_sdk"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "cremefraiche"},
|
||||
candidateAddition{AdditionalProducts: []string{"creme_fraiche"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "html-sanitizer"},
|
||||
candidateAddition{AdditionalProducts: []string{"html_sanitizer"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "sentry-raven"},
|
||||
candidateAddition{AdditionalProducts: []string{"raven-ruby"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "RedCloth"},
|
||||
candidateAddition{AdditionalProducts: []string{"redcloth_library"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "VladTheEnterprising"},
|
||||
candidateAddition{AdditionalProducts: []string{"vladtheenterprising"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "yajl-ruby"},
|
||||
candidateAddition{AdditionalProducts: []string{"yajl-ruby_gem"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "cgi"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "date"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "openssl"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "rake"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "rdoc"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "rexml"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "trunk"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "webrick"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
// Python packages
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "python-rrdtool"},
|
||||
candidateAddition{AdditionalProducts: []string{"rrdtool"}},
|
||||
},
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "cryptography"},
|
||||
candidateAddition{AdditionalProducts: []string{"python-cryptography"}, AdditionalVendors: []string{"python-cryptography_project"}},
|
||||
},
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "pip"},
|
||||
candidateAddition{AdditionalVendors: []string{"pypa"}},
|
||||
},
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "Django"},
|
||||
candidateAddition{AdditionalVendors: []string{"djangoproject"}},
|
||||
},
|
||||
// Alpine packages
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "curl"},
|
||||
candidateAddition{AdditionalVendors: []string{"haxx"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "python3"},
|
||||
candidateAddition{AdditionalProducts: []string{"python"}, AdditionalVendors: []string{"python", "python_software_foundation"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "python"},
|
||||
candidateAddition{AdditionalVendors: []string{"python_software_foundation"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "nodejs"},
|
||||
candidateAddition{AdditionalProducts: []string{"node.js"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "nodejs-current"},
|
||||
candidateAddition{AdditionalProducts: []string{"node.js"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "go"},
|
||||
candidateAddition{AdditionalVendors: []string{"golang"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "ruby"},
|
||||
candidateAddition{AdditionalVendors: []string{"ruby-lang"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "bazel"},
|
||||
candidateAddition{AdditionalVendors: []string{"google"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "clang"},
|
||||
candidateAddition{AdditionalVendors: []string{"llvm"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "openjdk"},
|
||||
candidateAddition{AdditionalVendors: []string{"oracle"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "glibc"},
|
||||
candidateAddition{AdditionalVendors: []string{"gnu"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "glib"},
|
||||
candidateAddition{AdditionalVendors: []string{"gnome"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "bash"},
|
||||
candidateAddition{AdditionalVendors: []string{"gnu"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "alsa-lib"},
|
||||
candidateAddition{AdditionalVendors: []string{"alsa-project"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "alsa"},
|
||||
candidateAddition{AdditionalVendors: []string{"alsa-project"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "make"},
|
||||
candidateAddition{AdditionalVendors: []string{"gnu"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "git"},
|
||||
candidateAddition{AdditionalVendors: []string{"git-scm"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "bind"},
|
||||
candidateAddition{AdditionalVendors: []string{"isc"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "libxpm"},
|
||||
candidateAddition{AdditionalVendors: []string{"libxpm_project"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "musl"},
|
||||
candidateAddition{AdditionalVendors: []string{"musl-libc"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "firefox"},
|
||||
candidateAddition{AdditionalVendors: []string{"mozilla"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "firefox-esr"},
|
||||
candidateAddition{AdditionalVendors: []string{"mozilla"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "thunderbird"},
|
||||
candidateAddition{AdditionalVendors: []string{"mozilla"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "chromium"},
|
||||
candidateAddition{AdditionalVendors: []string{"google"}, AdditionalProducts: []string{"chrome"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "apache"},
|
||||
candidateAddition{AdditionalProducts: []string{"http_server"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "tiff"},
|
||||
candidateAddition{AdditionalProducts: []string{"libtiff"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "ghostscript"},
|
||||
candidateAddition{AdditionalVendors: []string{"artifex"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "openjpeg"},
|
||||
candidateAddition{AdditionalVendors: []string{"uclouvain"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "xorg-server"},
|
||||
candidateAddition{AdditionalVendors: []string{"x.org"}, AdditionalProducts: []string{"x_server"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "podofo"},
|
||||
candidateAddition{AdditionalVendors: []string{"podofo_project"}},
|
||||
},
|
||||
{
|
||||
pkg.ApkPkg,
|
||||
candidateKey{PkgName: "wpa_supplicant"},
|
||||
candidateAddition{AdditionalVendors: []string{"w1.fi"}},
|
||||
},
|
||||
//
|
||||
// Binary packages
|
||||
{
|
||||
pkg.BinaryPkg,
|
||||
candidateKey{PkgName: "node"},
|
||||
candidateAddition{AdditionalProducts: []string{"nodejs", "node.js"}},
|
||||
},
|
||||
})
|
||||
|
||||
var defaultCandidateRemovals = buildCandidateRemovalLookup(
|
||||
[]candidateRemovalComposite{
|
||||
// Python packages
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "redis"},
|
||||
candidateRemovals{VendorsToRemove: []string{"redis"}},
|
||||
},
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "kubernetes"},
|
||||
candidateRemovals{ProductsToRemove: []string{"kubernetes"}},
|
||||
},
|
||||
// NPM packages
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "redis"},
|
||||
candidateRemovals{VendorsToRemove: []string{"redis"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "php"},
|
||||
candidateRemovals{VendorsToRemove: []string{"php"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "delegate"},
|
||||
candidateRemovals{VendorsToRemove: []string{"delegate"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "docker"},
|
||||
candidateRemovals{VendorsToRemove: []string{"docker"}},
|
||||
},
|
||||
// Java packages
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-builder-support"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-model"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-repository-metadata"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-settings"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-settings-builder"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-api"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-connector-basic"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-impl"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-named-locks"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-spi"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-transport-file"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-transport-http"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-transport-wagon"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-resolver-util"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "maven-shared-utils"},
|
||||
candidateRemovals{ProductsToRemove: []string{"maven"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "gradle-enterprise"},
|
||||
candidateRemovals{
|
||||
ProductsToRemove: []string{"gradle-enterprise"},
|
||||
VendorsToRemove: []string{"gradle"},
|
||||
},
|
||||
},
|
||||
// Ruby packages
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "redis"},
|
||||
candidateRemovals{ProductsToRemove: []string{"redis"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "grpc"},
|
||||
candidateRemovals{ProductsToRemove: []string{"grpc"}},
|
||||
},
|
||||
})
|
||||
|
||||
// buildCandidateLookup is a convenience function for creating the defaultCandidateAdditions set
|
||||
func buildCandidateLookup(cc []candidateComposite) (ca map[pkg.Type]map[candidateKey]candidateAddition) {
|
||||
ca = make(map[pkg.Type]map[candidateKey]candidateAddition)
|
||||
for _, c := range cc {
|
||||
if _, ok := ca[c.Type]; !ok {
|
||||
ca[c.Type] = make(map[candidateKey]candidateAddition)
|
||||
}
|
||||
ca[c.Type][c.candidateKey] = c.candidateAddition
|
||||
}
|
||||
|
||||
return ca
|
||||
}
|
||||
|
||||
// buildCandidateRemovalLookup is a convenience function for creating the defaultCandidateRemovals set
|
||||
func buildCandidateRemovalLookup(cc []candidateRemovalComposite) (ca map[pkg.Type]map[candidateKey]candidateRemovals) {
|
||||
ca = make(map[pkg.Type]map[candidateKey]candidateRemovals)
|
||||
for _, c := range cc {
|
||||
if _, ok := ca[c.Type]; !ok {
|
||||
ca[c.Type] = make(map[candidateKey]candidateRemovals)
|
||||
}
|
||||
ca[c.Type][c.candidateKey] = c.candidateRemovals
|
||||
}
|
||||
return ca
|
||||
}
|
||||
|
||||
// candidateKey represents the set of inputs that should be matched on in order to signal more candidate additions to be used.
|
||||
type candidateKey struct {
|
||||
Vendor string
|
||||
PkgName string
|
||||
}
|
||||
|
||||
// candidateRemovals are the specific removals that should be considered during CPE generation (given a specific candidateKey)
|
||||
type candidateRemovals struct {
|
||||
ProductsToRemove []string
|
||||
VendorsToRemove []string
|
||||
}
|
||||
|
||||
// candidateAddition are the specific additions that should be considered during CPE generation (given a specific candidateKey)
|
||||
type candidateAddition struct {
|
||||
AdditionalProducts []string
|
||||
AdditionalVendors []string
|
||||
}
|
||||
|
||||
// findAdditionalVendors searches all possible vendor additions that could be added during the CPE generation process (given package info + a vendor candidate)
|
||||
func findAdditionalVendors(allAdditions map[pkg.Type]map[candidateKey]candidateAddition, ty pkg.Type, pkgName, vendor string) (vendors []string) {
|
||||
additions, ok := allAdditions[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
Vendor: vendor,
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
Vendor: vendor,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
// findAdditionalProducts searches all possible product additions that could be added during the CPE generation process (given package info)
|
||||
func findAdditionalProducts(allAdditions map[pkg.Type]map[candidateKey]candidateAddition, ty pkg.Type, pkgName string) (products []string) {
|
||||
additions, ok := allAdditions[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
products = append(products, addition.AdditionalProducts...)
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
// findVendorsToRemove searches all possible vendor removals that could be removed during the CPE generation process (given package info + a vendor candidate)
|
||||
func findVendorsToRemove(allRemovals map[pkg.Type]map[candidateKey]candidateRemovals, ty pkg.Type, pkgName string) (vendors []string) {
|
||||
removals, ok := allRemovals[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if removal, ok := removals[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
vendors = append(vendors, removal.VendorsToRemove...)
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
// findProductsToRemove searches all possible product removals that could be removed during the CPE generation process (given package info)
|
||||
func findProductsToRemove(allRemovals map[pkg.Type]map[candidateKey]candidateRemovals, ty pkg.Type, pkgName string) (products []string) {
|
||||
removals, ok := allRemovals[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if removal, ok := removals[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
products = append(products, removal.ProductsToRemove...)
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_additionalProducts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allAdditions map[pkg.Type]map[candidateKey]candidateAddition
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "product name addition",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "spring-core",
|
||||
}: {
|
||||
AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "spring-core",
|
||||
expected: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
{
|
||||
name: "no addition found",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "spring-core",
|
||||
}: {
|
||||
AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "nothing",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, findAdditionalProducts(test.allAdditions, test.ty, test.pkgName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_additionalVendors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allAdditions map[pkg.Type]map[candidateKey]candidateAddition
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
vendor string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "vendor addition by input vendor",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "NOT-MY-PACKAGE",
|
||||
vendor: "my-vendor",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor addition by input package name",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
vendor: "NOT-MY-VENDOR",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor addition by input package name + vendor",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"one-good-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"another-good-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
vendor: "my-vendor",
|
||||
expected: []string{"awesome-vendor-addition", "one-good-addition", "another-good-addition"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, findAdditionalVendors(test.allAdditions, test.ty, test.pkgName, test.vendor))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findVendorsToRemove(t *testing.T) {
|
||||
//GIVEN
|
||||
tests := []struct {
|
||||
name string
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "vendor removal match by input package name",
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor removal miss by input package name",
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name-1",
|
||||
},
|
||||
}
|
||||
|
||||
allRemovals := map[pkg.Type]map[candidateKey]candidateRemovals{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
VendorsToRemove: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
//WHEN + THEN
|
||||
assert.Equal(t, test.expected, findVendorsToRemove(allRemovals, test.ty, test.pkgName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findProductsToRemove(t *testing.T) {
|
||||
//GIVEN
|
||||
tests := []struct {
|
||||
name string
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "vendor removal match by input package name",
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor removal miss by input package name",
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name-1",
|
||||
},
|
||||
}
|
||||
|
||||
allRemovals := map[pkg.Type]map[candidateKey]candidateRemovals{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
ProductsToRemove: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
//WHEN + THEN
|
||||
assert.Equal(t, test.expected, findProductsToRemove(allRemovals, test.ty, test.pkgName))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/cpegenerate"
|
||||
)
|
||||
|
||||
func Generate(p pkg.Package) []cpe.CPE {
|
||||
return cpegenerate.FromPackageAttributes(p)
|
||||
}
|
||||
|
||||
func DictionaryFind(p pkg.Package) (cpe.CPE, bool) {
|
||||
return cpegenerate.FromDictionaryFind(p)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
package dictionary
|
||||
|
||||
//go:generate go run ./index-generator/ -o data/cpe-index.json
|
||||
@@ -1,230 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common/cpe/dictionary"
|
||||
)
|
||||
|
||||
func generateIndexedDictionaryJSON(rawGzipData io.Reader) ([]byte, error) {
|
||||
gzipReader, err := gzip.NewReader(rawGzipData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decompress CPE dictionary: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
// Read XML data
|
||||
data, err := io.ReadAll(gzipReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read CPE dictionary: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal XML
|
||||
var cpeList CpeList
|
||||
if err := xml.Unmarshal(data, &cpeList); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal CPE dictionary XML: %w", err)
|
||||
}
|
||||
|
||||
// Filter out data that's not applicable here
|
||||
cpeList = filterCpeList(cpeList)
|
||||
|
||||
// Create indexed dictionary to help with looking up CPEs
|
||||
indexedDictionary := indexCPEList(cpeList)
|
||||
|
||||
// Convert to JSON
|
||||
jsonData, err := json.MarshalIndent(indexedDictionary, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to marshal CPE dictionary to JSON: %w", err)
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
// filterCpeList removes CPE items that are not applicable to software packages.
|
||||
func filterCpeList(cpeList CpeList) CpeList {
|
||||
var processedCpeList CpeList
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, cpeItem := range cpeList.CpeItems {
|
||||
// Skip CPE items that don't have any references.
|
||||
if len(cpeItem.References) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip CPE items where the CPE URI doesn't meet our criteria.
|
||||
parsedName, err := wfn.Parse(cpeItem.Name)
|
||||
if err != nil {
|
||||
log.Printf("unable to parse CPE URI %q: %s", cpeItem.Name, err)
|
||||
}
|
||||
|
||||
if slices.Contains([]string{"h", "o"}, parsedName.Part) {
|
||||
continue
|
||||
}
|
||||
|
||||
normalizedName := normalizeCPE(parsedName).BindToURI()
|
||||
if _, ok := seen[normalizedName]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalizedName] = struct{}{}
|
||||
cpeItem.Name = normalizedName
|
||||
|
||||
parsedCPE, err := wfn.Parse(cpeItem.Cpe23Item.Name)
|
||||
if err != nil {
|
||||
log.Printf("unable to parse CPE value %q: %s", cpeItem.Cpe23Item.Name, err)
|
||||
}
|
||||
|
||||
cpeItem.Cpe23Item.Name = normalizeCPE(parsedCPE).BindToFmtString()
|
||||
|
||||
processedCpeList.CpeItems = append(processedCpeList.CpeItems, cpeItem)
|
||||
}
|
||||
|
||||
return processedCpeList
|
||||
}
|
||||
|
||||
// normalizeCPE removes the version and update parts of a CPE.
|
||||
func normalizeCPE(cpe *wfn.Attributes) *wfn.Attributes {
|
||||
cpeCopy := *cpe
|
||||
|
||||
cpeCopy.Version = ""
|
||||
cpeCopy.Update = ""
|
||||
|
||||
return &cpeCopy
|
||||
}
|
||||
|
||||
const (
|
||||
prefixForNPMPackages = "https://www.npmjs.com/package/"
|
||||
prefixForRubyGems = "https://rubygems.org/gems/"
|
||||
prefixForRubyGemsHTTP = "http://rubygems.org/gems/"
|
||||
prefixForNativeRubyGems = "https://github.com/ruby/"
|
||||
prefixForPyPIPackages = "https://pypi.org/project/"
|
||||
prefixForJenkinsPlugins = "https://github.com/jenkinsci/"
|
||||
prefixForRustCrates = "https://crates.io/crates/"
|
||||
)
|
||||
|
||||
// indexCPEList creates an index of CPEs by ecosystem.
|
||||
func indexCPEList(list CpeList) *dictionary.Indexed {
|
||||
indexed := &dictionary.Indexed{
|
||||
EcosystemPackages: make(map[string]dictionary.Packages),
|
||||
}
|
||||
|
||||
for _, cpeItem := range list.CpeItems {
|
||||
cpeItemName := cpeItem.Cpe23Item.Name
|
||||
|
||||
for _, reference := range cpeItem.References {
|
||||
ref := reference.Reference.Href
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ref, prefixForNPMPackages):
|
||||
addEntryForNPMPackage(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForRubyGems), strings.HasPrefix(ref, prefixForRubyGemsHTTP):
|
||||
addEntryForRubyGem(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForNativeRubyGems):
|
||||
addEntryForNativeRubyGem(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForPyPIPackages):
|
||||
addEntryForPyPIPackage(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForJenkinsPlugins):
|
||||
// It _might_ be a jenkins plugin!
|
||||
addEntryForJenkinsPlugin(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForRustCrates):
|
||||
addEntryForRustCrate(indexed, ref, cpeItemName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indexed
|
||||
}
|
||||
|
||||
func addEntryForRustCrate(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForRustCrates)
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemRustCrates]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRustCrates] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRustCrates][ref] = cpeItemName
|
||||
}
|
||||
|
||||
func addEntryForJenkinsPlugin(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForJenkinsPlugins)
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
|
||||
if !strings.HasSuffix(ref, "-plugin") {
|
||||
// It's not a jenkins plugin!
|
||||
return
|
||||
}
|
||||
|
||||
ref = strings.TrimSuffix(ref, "-plugin")
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemJenkinsPlugins]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemJenkinsPlugins] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemJenkinsPlugins][ref] = cpeItemName
|
||||
}
|
||||
|
||||
func addEntryForPyPIPackage(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForPyPIPackages)
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemPyPI]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemPyPI] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemPyPI][ref] = cpeItemName
|
||||
}
|
||||
|
||||
func addEntryForNativeRubyGem(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForNativeRubyGems)
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemRubyGems]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRubyGems] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRubyGems][ref] = cpeItemName
|
||||
}
|
||||
|
||||
func addEntryForRubyGem(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForRubyGems)
|
||||
ref = strings.TrimPrefix(ref, prefixForRubyGemsHTTP)
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemRubyGems]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRubyGems] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemRubyGems][ref] = cpeItemName
|
||||
}
|
||||
|
||||
func addEntryForNPMPackage(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.Split(ref, "/v/")[0]
|
||||
ref = strings.Split(ref, "?")[0]
|
||||
ref = strings.TrimPrefix(ref, prefixForNPMPackages)
|
||||
|
||||
if _, ok := indexed.EcosystemPackages[dictionary.EcosystemNPM]; !ok {
|
||||
indexed.EcosystemPackages[dictionary.EcosystemNPM] = make(dictionary.Packages)
|
||||
}
|
||||
|
||||
indexed.EcosystemPackages[dictionary.EcosystemNPM][ref] = cpeItemName
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common/cpe/dictionary"
|
||||
)
|
||||
|
||||
func Test_generateIndexedDictionaryJSON(t *testing.T) {
|
||||
f, err := os.Open("testdata/official-cpe-dictionary_v2.3.xml")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a buffer to store the gzipped data in memory
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w := gzip.NewWriter(buf)
|
||||
_, err = io.Copy(w, f)
|
||||
require.NoError(t, err)
|
||||
|
||||
// (finalize the gzip stream)
|
||||
err = w.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
dictionaryJSON, err := generateIndexedDictionaryJSON(buf)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile("./testdata/expected-cpe-index.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedDictionaryJSONString := string(expected)
|
||||
dictionaryJSONString := string(dictionaryJSON)
|
||||
|
||||
if diff := cmp.Diff(expectedDictionaryJSONString, dictionaryJSONString); diff != "" {
|
||||
t.Errorf("generateIndexedDictionaryJSON() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addEntryFuncs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
addEntryFunc func(indexed *dictionary.Indexed, ref string, cpeItemName string)
|
||||
inputRef string
|
||||
inputCpeItemName string
|
||||
expectedIndexed dictionary.Indexed
|
||||
}{
|
||||
{
|
||||
name: "addEntryForRustCrate",
|
||||
addEntryFunc: addEntryForRustCrate,
|
||||
inputRef: "https://crates.io/crates/unicycle/versions",
|
||||
inputCpeItemName: "cpe:2.3:a:unicycle_project:unicycle:*:*:*:*:*:rust:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemRustCrates: {
|
||||
"unicycle": "cpe:2.3:a:unicycle_project:unicycle:*:*:*:*:*:rust:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForJenkinsPlugin",
|
||||
addEntryFunc: addEntryForJenkinsPlugin,
|
||||
inputRef: "https://github.com/jenkinsci/sonarqube-plugin",
|
||||
inputCpeItemName: "cpe:2.3:a:sonarsource:sonarqube_scanner:2.7:*:*:*:*:jenkins:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemJenkinsPlugins: {
|
||||
"sonarqube": "cpe:2.3:a:sonarsource:sonarqube_scanner:2.7:*:*:*:*:jenkins:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForJenkinsPlugin: not actually a plugin",
|
||||
addEntryFunc: addEntryForJenkinsPlugin,
|
||||
inputRef: "https://github.com/jenkinsci/jenkins",
|
||||
inputCpeItemName: "cpe:2.3:a:jenkins:jenkinsci:2.7:*:*:*:*:*:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForPyPIPackage",
|
||||
addEntryFunc: addEntryForPyPIPackage,
|
||||
inputRef: "https://pypi.org/project/vault-cli/#history",
|
||||
inputCpeItemName: "cpe:2.3:a:vault-cli_project:vault-cli:*:*:*:*:*:python:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemPyPI: {
|
||||
"vault-cli": "cpe:2.3:a:vault-cli_project:vault-cli:*:*:*:*:*:python:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForNativeRubyGem",
|
||||
addEntryFunc: addEntryForNativeRubyGem,
|
||||
inputRef: "https://github.com/ruby/openssl/releases",
|
||||
inputCpeItemName: "cpe:2.3:a:ruby-lang:openssl:-:*:*:*:*:ruby:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemRubyGems: {
|
||||
"openssl": "cpe:2.3:a:ruby-lang:openssl:-:*:*:*:*:ruby:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForRubyGem: https",
|
||||
addEntryFunc: addEntryForRubyGem,
|
||||
inputRef: "https://rubygems.org/gems/actionview/versions",
|
||||
inputCpeItemName: "cpe:2.3:a:action_view_project:action_view:*:*:*:*:*:ruby:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemRubyGems: {
|
||||
"actionview": "cpe:2.3:a:action_view_project:action_view:*:*:*:*:*:ruby:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForRubyGem: http",
|
||||
addEntryFunc: addEntryForRubyGem,
|
||||
inputRef: "http://rubygems.org/gems/rbovirt",
|
||||
inputCpeItemName: "cpe:2.3:a:amos_benari:rbovirt:*:*:*:*:*:ruby:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemRubyGems: {
|
||||
"rbovirt": "cpe:2.3:a:amos_benari:rbovirt:*:*:*:*:*:ruby:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForNPMPackage",
|
||||
addEntryFunc: addEntryForNPMPackage,
|
||||
inputRef: "https://www.npmjs.com/package/@nubosoftware/node-static",
|
||||
inputCpeItemName: "cpe:2.3:a:\\@nubosoftware\\/node-static_project:\\@nubosoftware\\/node-static:-:*:*:*:*:node.js:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemNPM: {
|
||||
"@nubosoftware/node-static": "cpe:2.3:a:\\@nubosoftware\\/node-static_project:\\@nubosoftware\\/node-static:-:*:*:*:*:node.js:*:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
indexed := &dictionary.Indexed{
|
||||
EcosystemPackages: make(map[string]dictionary.Packages),
|
||||
}
|
||||
|
||||
tt.addEntryFunc(indexed, tt.inputRef, tt.inputCpeItemName)
|
||||
|
||||
if diff := cmp.Diff(tt.expectedIndexed, *indexed); diff != "" {
|
||||
t.Errorf("addEntry* mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// This program downloads the latest CPE dictionary from NIST and processes it into a JSON file that can be embedded into Syft for more accurate CPE results.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func mainE() error {
|
||||
var outputFilename string
|
||||
flag.StringVar(&outputFilename, "o", "", "file location to save CPE index")
|
||||
flag.Parse()
|
||||
|
||||
if outputFilename == "" {
|
||||
return errors.New("-o is required")
|
||||
}
|
||||
|
||||
// Download and decompress file
|
||||
fmt.Println("Fetching CPE dictionary...")
|
||||
resp, err := http.Get(cpeDictionaryURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get CPE dictionary: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Generating index...")
|
||||
dictionaryJSON, err := generateIndexedDictionaryJSON(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write CPE index (JSON data) to disk
|
||||
err = os.WriteFile(outputFilename, dictionaryJSON, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write processed CPE dictionary to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Done!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// errExit prints an error and exits with a non-zero exit code.
|
||||
func errExit(err error) {
|
||||
log.Printf("command failed: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := mainE(); err != nil {
|
||||
errExit(err)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package main
|
||||
|
||||
type CpeItem struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Title string `xml:"title"`
|
||||
References []struct {
|
||||
Reference struct {
|
||||
Href string `xml:"href,attr"`
|
||||
Body string `xml:",chardata"`
|
||||
} `xml:"reference"`
|
||||
} `xml:"references"`
|
||||
Cpe23Item struct {
|
||||
Name string `xml:"name,attr"`
|
||||
} `xml:"cpe23-item"`
|
||||
}
|
||||
|
||||
type CpeList struct {
|
||||
CpeItems []CpeItem `xml:"cpe-item"`
|
||||
}
|
||||
|
||||
const cpeDictionaryURL = "https://nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.3.xml.gz"
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"ecosystems": {
|
||||
"jenkins_plugins": {
|
||||
"sonarqube": "cpe:2.3:a:sonarsource:sonarqube_scanner:*:*:*:*:*:jenkins:*:*"
|
||||
},
|
||||
"npm": {
|
||||
"merge-recursive": "cpe:2.3:a:umbraengineering:merge-recursive:*:*:*:*:*:node.js:*:*",
|
||||
"static-dev-server": "cpe:2.3:a:static-dev-server_project:static-dev-server:*:*:*:*:*:node.js:*:*",
|
||||
"umount": "cpe:2.3:a:umount_project:umount:*:*:*:*:*:node.js:*:*",
|
||||
"undefsafe": "cpe:2.3:a:undefsafe_project:undefsafe:*:*:*:*:*:node.js:*:*",
|
||||
"underscore": "cpe:2.3:a:underscorejs:underscore:*:*:*:*:*:node.js:*:*",
|
||||
"underscore-99xp": "cpe:2.3:a:underscore-99xp_project:underscore-99xp:*:*:*:*:*:node.js:*:*",
|
||||
"unicode": "cpe:2.3:a:unicode_project:unicode:*:*:*:*:*:node.js:*:*",
|
||||
"unicorn-list": "cpe:2.3:a:unicorn-list_project:unicorn-list:*:*:*:*:*:node.js:*:*"
|
||||
},
|
||||
"rubygems": {
|
||||
"openssl": "cpe:2.3:a:ruby-lang:openssl:*:*:*:*:*:*:*:*"
|
||||
},
|
||||
"rust_crates": {
|
||||
"unicycle": "cpe:2.3:a:unicycle_project:unicycle:*:*:*:*:*:rust:*:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-24876
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
package dictionary
|
||||
|
||||
const (
|
||||
EcosystemNPM = "npm"
|
||||
EcosystemRubyGems = "rubygems"
|
||||
EcosystemPyPI = "pypi"
|
||||
EcosystemJenkinsPlugins = "jenkins_plugins"
|
||||
EcosystemRustCrates = "rust_crates"
|
||||
)
|
||||
|
||||
type Indexed struct {
|
||||
EcosystemPackages map[string]Packages `json:"ecosystems"`
|
||||
}
|
||||
|
||||
type Packages map[string]string
|
||||
@@ -1,112 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
// fieldCandidate represents a single "guess" for a specific field in a future CPE (vendor, product, target SW, etc).
|
||||
// When generating these candidates depending on the field the value was sourced from there may be only a subset of
|
||||
// transforms that should be applied (downstream of extraction). Expressing candidates in this struct allows for this
|
||||
// flexibility such that downstream transforms can be elected into or skipped over.
|
||||
type fieldCandidate struct {
|
||||
value string
|
||||
disallowSubSelections bool
|
||||
disallowDelimiterVariations bool
|
||||
}
|
||||
|
||||
type fieldCandidateSet map[fieldCandidate]struct{}
|
||||
|
||||
func newFieldCandidateSetFromSets(sets ...fieldCandidateSet) fieldCandidateSet {
|
||||
s := newFieldCandidateSet()
|
||||
for _, set := range sets {
|
||||
s.add(set.list()...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func newFieldCandidateSet(values ...string) fieldCandidateSet {
|
||||
s := make(fieldCandidateSet)
|
||||
s.addValue(values...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) addValue(values ...string) {
|
||||
for _, value := range values {
|
||||
// default candidate as an allow-all
|
||||
candidate := fieldCandidate{
|
||||
value: cleanCandidateField(value),
|
||||
}
|
||||
s[candidate] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) add(candidates ...fieldCandidate) {
|
||||
for _, candidate := range candidates {
|
||||
candidate.value = cleanCandidateField(candidate.value)
|
||||
s[candidate] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) removeByValue(values ...string) {
|
||||
for _, value := range values {
|
||||
s.removeWhere(valueEquals(value))
|
||||
}
|
||||
}
|
||||
|
||||
// removeWhere removes all entries from the fieldCandidateSet for which the condition function returns true.
|
||||
func (s fieldCandidateSet) removeWhere(condition fieldCandidateCondition) {
|
||||
for candidate := range s {
|
||||
if condition(candidate) {
|
||||
delete(s, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) clear() {
|
||||
for k := range s {
|
||||
delete(s, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) union(others ...fieldCandidateSet) {
|
||||
for _, other := range others {
|
||||
s.add(other.list()...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) list() (results []fieldCandidate) {
|
||||
for c := range s {
|
||||
results = append(results, c)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) values() (results []string) {
|
||||
for _, c := range s.list() {
|
||||
results = append(results, c.value)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) uniqueValues() []string {
|
||||
return strset.New(s.values()...).List()
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) copy() fieldCandidateSet {
|
||||
newSet := newFieldCandidateSet()
|
||||
newSet.add(s.list()...)
|
||||
|
||||
return newSet
|
||||
}
|
||||
|
||||
func cleanCandidateField(field string) string {
|
||||
cleanedValue, err := strconv.Unquote(field)
|
||||
if err != nil {
|
||||
return field
|
||||
}
|
||||
return cleanedValue
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package cpe
|
||||
|
||||
// A fieldCandidateCondition returns true if the condition is true for a given fieldCandidate.
|
||||
type fieldCandidateCondition func(fieldCandidate) bool
|
||||
|
||||
func subSelectionsDisallowed(c fieldCandidate) bool {
|
||||
return c.disallowSubSelections
|
||||
}
|
||||
|
||||
func delimiterVariationsDisallowed(c fieldCandidate) bool {
|
||||
return c.disallowDelimiterVariations
|
||||
}
|
||||
|
||||
func valueEquals(v string) fieldCandidateCondition {
|
||||
return func(candidate fieldCandidate) bool {
|
||||
return candidate.value == v
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []fieldCandidate
|
||||
exclusionConditions []fieldCandidateCondition
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
input: []fieldCandidate{
|
||||
{
|
||||
value: "allow anything",
|
||||
},
|
||||
{
|
||||
value: "no-sub-selections",
|
||||
disallowSubSelections: true,
|
||||
},
|
||||
{
|
||||
value: "no-delimiter-variations",
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
{
|
||||
value: "allow nothing",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
"no-sub-selections",
|
||||
"no-delimiter-variations",
|
||||
"allow nothing",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter out sub-selections",
|
||||
input: []fieldCandidate{
|
||||
{
|
||||
value: "allow anything",
|
||||
},
|
||||
{
|
||||
value: "no-sub-selections",
|
||||
disallowSubSelections: true,
|
||||
},
|
||||
{
|
||||
value: "no-delimiter-variations",
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
{
|
||||
value: "allow nothing",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
subSelectionsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
"no-delimiter-variations",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter out delimiter-variations",
|
||||
input: []fieldCandidate{
|
||||
{
|
||||
value: "allow anything",
|
||||
},
|
||||
{
|
||||
value: "no-sub-selections",
|
||||
disallowSubSelections: true,
|
||||
},
|
||||
{
|
||||
value: "no-delimiter-variations",
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
{
|
||||
value: "allow nothing",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
delimiterVariationsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
"no-sub-selections",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all exclusionConditions",
|
||||
input: []fieldCandidate{
|
||||
{
|
||||
value: "allow anything",
|
||||
},
|
||||
{
|
||||
value: "no-sub-selections",
|
||||
disallowSubSelections: true,
|
||||
},
|
||||
{
|
||||
value: "no-delimiter-variations",
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
{
|
||||
value: "allow nothing",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
delimiterVariationsDisallowed,
|
||||
subSelectionsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
set := newFieldCandidateSet()
|
||||
set.add(test.input...)
|
||||
|
||||
for _, condition := range test.exclusionConditions {
|
||||
set.removeWhere(condition)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, test.expect, set.values())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_addValue(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
// we should clean all values (unquote strings)
|
||||
s.addValue(`"string!"`)
|
||||
assert.ElementsMatch(t, []string{"string!"}, s.values())
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_add(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
// we should clean all values (unquote strings)
|
||||
s.add(fieldCandidate{
|
||||
value: `"string!"`,
|
||||
})
|
||||
assert.ElementsMatch(t, []string{"string!"}, s.values())
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_clear(t *testing.T) {
|
||||
s := newFieldCandidateSet("1", "2")
|
||||
assert.NotEmpty(t, s.values())
|
||||
s.clear()
|
||||
assert.Empty(t, s.values())
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_union(t *testing.T) {
|
||||
s1 := newFieldCandidateSet("1", "2")
|
||||
assert.Len(t, s1.list(), 2)
|
||||
s2 := newFieldCandidateSet("2", "3", "4")
|
||||
assert.Len(t, s2.list(), 3)
|
||||
s3 := newFieldCandidateSet()
|
||||
s3.add(
|
||||
fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: false,
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "4",
|
||||
disallowSubSelections: false,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "5",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
)
|
||||
assert.Len(t, s3.list(), 3)
|
||||
|
||||
s1.union(s2, s3)
|
||||
|
||||
// 1 & 4 have duplicate entries since there are candidate conditions set
|
||||
assert.ElementsMatch(t, s1.values(), []string{"1", "1", "2", "3", "4", "4", "5"})
|
||||
|
||||
assert.ElementsMatch(t, s1.list(), []fieldCandidate{
|
||||
{
|
||||
value: "1",
|
||||
},
|
||||
{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: false,
|
||||
},
|
||||
{
|
||||
value: "2",
|
||||
},
|
||||
{
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
value: "4",
|
||||
},
|
||||
{
|
||||
value: "4",
|
||||
disallowSubSelections: false,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
{
|
||||
value: "5",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_union_byValue(t *testing.T) {
|
||||
s1 := newFieldCandidateSet("1", "2")
|
||||
assert.Len(t, s1.list(), 2)
|
||||
s2 := newFieldCandidateSet("2", "3", "4")
|
||||
assert.Len(t, s2.list(), 3)
|
||||
s3 := newFieldCandidateSet("1", "4", "5")
|
||||
assert.Len(t, s3.list(), 3)
|
||||
|
||||
s1.union(s2, s3)
|
||||
|
||||
assert.ElementsMatch(t, s1.values(), []string{"1", "2", "3", "4", "5"})
|
||||
|
||||
assert.ElementsMatch(t, s1.list(), []fieldCandidate{
|
||||
{
|
||||
value: "1",
|
||||
},
|
||||
{
|
||||
value: "2",
|
||||
},
|
||||
{
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
value: "4",
|
||||
},
|
||||
{
|
||||
value: "5",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_uniqueValues(t *testing.T) {
|
||||
set := newFieldCandidateSet()
|
||||
set.add(
|
||||
fieldCandidate{
|
||||
value: "1",
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "2",
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "2",
|
||||
},
|
||||
fieldCandidate{
|
||||
value: "3",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
)
|
||||
|
||||
assert.ElementsMatch(t, []string{"1", "2", "3"}, set.uniqueValues())
|
||||
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_removeByValue(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
|
||||
// should be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
})
|
||||
|
||||
// should not be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "2",
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 5)
|
||||
|
||||
s.removeByValue("1")
|
||||
|
||||
assert.Len(t, s.values(), 1)
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_removeByCondition(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
|
||||
// should be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "hello-world",
|
||||
})
|
||||
|
||||
// should not be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "2",
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 3)
|
||||
|
||||
s.removeWhere(func(candidate fieldCandidate) bool {
|
||||
return candidate.disallowSubSelections == true
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 2)
|
||||
|
||||
s.removeWhere(func(candidate fieldCandidate) bool {
|
||||
return strings.Contains(candidate.value, "-")
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 1)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
const jenkinsName = "jenkins"
|
||||
|
||||
// filterFn instances should return true if the given CPE should be removed from a collection for the given package
|
||||
type filterFn func(cpe cpe.CPE, p pkg.Package) bool
|
||||
|
||||
var cpeFilters = []filterFn{
|
||||
disallowJiraClientServerMismatch,
|
||||
disallowJenkinsServerCPEForPluginPackage,
|
||||
disallowJenkinsCPEsNotAssociatedWithJenkins,
|
||||
disallowNonParseableCPEs,
|
||||
}
|
||||
|
||||
func filter(cpes []cpe.CPE, p pkg.Package, filters ...filterFn) (result []cpe.CPE) {
|
||||
cpeLoop:
|
||||
for _, c := range cpes {
|
||||
for _, fn := range filters {
|
||||
if fn(c, p) {
|
||||
continue cpeLoop
|
||||
}
|
||||
}
|
||||
// all filter functions passed on filtering this CPE
|
||||
result = append(result, c)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool {
|
||||
v := c.String()
|
||||
_, err := cpe.New(v)
|
||||
|
||||
cannotParse := err != nil
|
||||
|
||||
return cannotParse
|
||||
}
|
||||
|
||||
// jenkins plugins should not match against jenkins
|
||||
func disallowJenkinsServerCPEForPluginPackage(cpe cpe.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 disallowJenkinsCPEsNotAssociatedWithJenkins(cpe cpe.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 disallowJiraClientServerMismatch(cpe cpe.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
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cpe cpe.CPE
|
||||
pkg pkg.Package
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "go case (filter out)",
|
||||
cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
pkg: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ignore jenkins plugins with unique name",
|
||||
cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
pkg: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ignore java packages",
|
||||
cpe: cpe.Must("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, disallowJenkinsServerCPEForPluginPackage(test.cpe, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cpe cpe.CPE
|
||||
pkg pkg.Package
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "filter out mismatched name (cloudbees vendor)",
|
||||
cpe: cpe.Must("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: cpe.Must("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: cpe.Must("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: cpe.Must("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: cpe.Must("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, disallowJenkinsCPEsNotAssociatedWithJenkins(test.cpe, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cpe cpe.CPE
|
||||
pkg pkg.Package
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "filter out mismatched name (atlassian vendor)",
|
||||
cpe: cpe.Must("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: cpe.Must("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: cpe.Must("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: cpe.Must("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: cpe.Must("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, disallowJiraClientServerMismatch(test.cpe, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common/cpe/dictionary"
|
||||
)
|
||||
|
||||
// knownVendors contains vendor strings that are known to exist in
|
||||
// the CPE database, so they will be preferred over other candidates:
|
||||
var knownVendors = strset.New("apache")
|
||||
|
||||
func newCPE(product, vendor, version, targetSW string) *cpe.CPE {
|
||||
c := cpe.NewWithAny()
|
||||
c.Part = "a"
|
||||
c.Product = product
|
||||
c.Vendor = vendor
|
||||
c.Version = version
|
||||
c.TargetSW = targetSW
|
||||
if cpe.ValidateString(c.String()) != nil {
|
||||
return nil
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
//go:embed dictionary/data/cpe-index.json
|
||||
var indexedCPEDictionaryData []byte
|
||||
|
||||
var indexedCPEDictionary *dictionary.Indexed
|
||||
var indexedCPEDictionaryOnce sync.Once
|
||||
|
||||
func GetIndexedDictionary() (_ *dictionary.Indexed, err error) {
|
||||
indexedCPEDictionaryOnce.Do(func() {
|
||||
err = json.Unmarshal(indexedCPEDictionaryData, &indexedCPEDictionary)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if indexedCPEDictionary == nil {
|
||||
err = fmt.Errorf("failed to unmarshal indexed CPE dictionary")
|
||||
return
|
||||
}
|
||||
|
||||
return indexedCPEDictionary, err
|
||||
}
|
||||
|
||||
func DictionaryFind(p pkg.Package) (cpe.CPE, bool) {
|
||||
dict, err := GetIndexedDictionary()
|
||||
if err != nil {
|
||||
log.Debugf("dictionary CPE lookup not available: %+v", err)
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
var (
|
||||
cpeString string
|
||||
ok bool
|
||||
)
|
||||
|
||||
switch p.Type {
|
||||
case pkg.NpmPkg:
|
||||
cpeString, ok = dict.EcosystemPackages[dictionary.EcosystemNPM][p.Name]
|
||||
|
||||
case pkg.GemPkg:
|
||||
cpeString, ok = dict.EcosystemPackages[dictionary.EcosystemRubyGems][p.Name]
|
||||
|
||||
case pkg.PythonPkg:
|
||||
cpeString, ok = dict.EcosystemPackages[dictionary.EcosystemPyPI][p.Name]
|
||||
|
||||
case pkg.JenkinsPluginPkg:
|
||||
cpeString, ok = dict.EcosystemPackages[dictionary.EcosystemJenkinsPlugins][p.Name]
|
||||
|
||||
case pkg.RustPkg:
|
||||
cpeString, ok = dict.EcosystemPackages[dictionary.EcosystemRustCrates][p.Name]
|
||||
|
||||
default:
|
||||
// The dictionary doesn't support this package type yet.
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// The dictionary doesn't have a CPE for this package.
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
parsedCPE, err := cpe.New(cpeString)
|
||||
if err != nil {
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
parsedCPE.Version = p.Version
|
||||
|
||||
return parsedCPE, true
|
||||
}
|
||||
|
||||
// Generate Create a list of CPEs for a given package, trying to guess the vendor, product tuple. We should be trying to
|
||||
// generate the minimal set of representative CPEs, which implies that optional fields should not be included
|
||||
// (such as target SW).
|
||||
func Generate(p pkg.Package) []cpe.CPE {
|
||||
vendors := candidateVendors(p)
|
||||
products := candidateProducts(p)
|
||||
if len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := strset.New()
|
||||
cpes := make([]cpe.CPE, 0)
|
||||
for _, product := range products {
|
||||
for _, vendor := range vendors {
|
||||
// prevent duplicate entries...
|
||||
key := fmt.Sprintf("%s|%s|%s", product, vendor, p.Version)
|
||||
if keys.Has(key) {
|
||||
continue
|
||||
}
|
||||
keys.Add(key)
|
||||
// add a new entry...
|
||||
if c := newCPE(product, vendor, p.Version, wfn.Any); c != nil {
|
||||
cpes = append(cpes, *c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter out any known combinations that don't accurately represent this package
|
||||
cpes = filter(cpes, p, cpeFilters...)
|
||||
|
||||
sort.Sort(cpe.BySpecificity(cpes))
|
||||
|
||||
return cpes
|
||||
}
|
||||
|
||||
func candidateVendors(p pkg.Package) []string {
|
||||
// in ecosystems where the packaging metadata does not have a clear field to indicate a vendor (or a field that
|
||||
// could be interpreted indirectly as such) the project name tends to be a common stand in. Examples of this
|
||||
// are the elasticsearch gem, xstream jar, and rack gem... all of these cases you can find vulnerabilities
|
||||
// with CPEs where the vendor is the product name and doesn't appear to be derived from any available package
|
||||
// metadata.
|
||||
vendors := newFieldCandidateSet(candidateProducts(p)...)
|
||||
|
||||
switch p.Language {
|
||||
case pkg.JavaScript:
|
||||
// for JavaScript if we find node.js as a package then the vendor is "nodejs"
|
||||
if p.Name == "node.js" {
|
||||
vendors.addValue("nodejs")
|
||||
}
|
||||
case pkg.Ruby:
|
||||
vendors.addValue("ruby-lang")
|
||||
case pkg.Go:
|
||||
// replace all candidates with only the golang-specific helper
|
||||
vendors.clear()
|
||||
|
||||
vendor := candidateVendorForGo(p.Name)
|
||||
if vendor != "" {
|
||||
vendors.addValue(vendor)
|
||||
}
|
||||
}
|
||||
|
||||
switch p.Metadata.(type) {
|
||||
case pkg.RpmDBEntry:
|
||||
vendors.union(candidateVendorsForRPM(p))
|
||||
case pkg.RubyGemspec:
|
||||
vendors.union(candidateVendorsForRuby(p))
|
||||
case pkg.PythonPackage:
|
||||
vendors.union(candidateVendorsForPython(p))
|
||||
case pkg.JavaArchive:
|
||||
vendors.union(candidateVendorsForJava(p))
|
||||
case pkg.ApkDBEntry:
|
||||
vendors.union(candidateVendorsForAPK(p))
|
||||
case pkg.NpmPackage:
|
||||
vendors.union(candidateVendorsForJavascript(p))
|
||||
}
|
||||
|
||||
// We should no longer be generating vendor candidates with these values ["" and "*"]
|
||||
// (since CPEs will match any other value)
|
||||
vendors.removeByValue("")
|
||||
vendors.removeByValue("*")
|
||||
|
||||
// try swapping hyphens for underscores, vice versa, and removing separators altogether
|
||||
addDelimiterVariations(vendors)
|
||||
|
||||
// generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci])
|
||||
addAllSubSelections(vendors)
|
||||
|
||||
// add more candidates based on the package info for each vendor candidate
|
||||
for _, vendor := range vendors.uniqueValues() {
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, p.Type, p.Name, vendor)...)
|
||||
}
|
||||
|
||||
// remove known mis
|
||||
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, p.Type, p.Name)...)
|
||||
|
||||
uniqueVendors := vendors.uniqueValues()
|
||||
|
||||
// if any known vendor was detected, pick that one.
|
||||
for _, vendor := range uniqueVendors {
|
||||
if knownVendors.Has(vendor) {
|
||||
return []string{vendor}
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueVendors
|
||||
}
|
||||
|
||||
func candidateProducts(p pkg.Package) []string {
|
||||
products := newFieldCandidateSet(p.Name)
|
||||
|
||||
_, hasJavaMetadata := p.Metadata.(pkg.JavaArchive)
|
||||
|
||||
switch {
|
||||
case p.Language == pkg.Python:
|
||||
if !strings.HasPrefix(p.Name, "python") {
|
||||
products.addValue("python-" + p.Name)
|
||||
}
|
||||
case p.Language == pkg.Java || hasJavaMetadata:
|
||||
products.addValue(candidateProductsForJava(p)...)
|
||||
case p.Language == pkg.Go:
|
||||
// replace all candidates with only the golang-specific helper
|
||||
products.clear()
|
||||
|
||||
prod := candidateProductForGo(p.Name)
|
||||
if prod != "" {
|
||||
products.addValue(prod)
|
||||
}
|
||||
}
|
||||
|
||||
if _, hasAPKMetadata := p.Metadata.(pkg.ApkDBEntry); hasAPKMetadata {
|
||||
products.union(candidateProductsForAPK(p))
|
||||
}
|
||||
|
||||
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||
products.removeByValue("")
|
||||
products.removeByValue("*")
|
||||
|
||||
// try swapping hyphens for underscores, vice versa, and removing separators altogether
|
||||
addDelimiterVariations(products)
|
||||
|
||||
// add known candidate additions
|
||||
products.addValue(findAdditionalProducts(defaultCandidateAdditions, p.Type, p.Name)...)
|
||||
|
||||
// remove known candidate removals
|
||||
products.removeByValue(findProductsToRemove(defaultCandidateRemovals, p.Type, p.Name)...)
|
||||
|
||||
return products.uniqueValues()
|
||||
}
|
||||
|
||||
func addAllSubSelections(fields fieldCandidateSet) {
|
||||
candidatesForVariations := fields.copy()
|
||||
candidatesForVariations.removeWhere(subSelectionsDisallowed)
|
||||
|
||||
for _, candidate := range candidatesForVariations.values() {
|
||||
fields.addValue(generateSubSelections(candidate)...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 addDelimiterVariations(fields fieldCandidateSet) {
|
||||
candidatesForVariations := fields.copy()
|
||||
candidatesForVariations.removeWhere(delimiterVariationsDisallowed)
|
||||
|
||||
for _, candidate := range candidatesForVariations.list() {
|
||||
field := candidate.value
|
||||
hasHyphen := strings.Contains(field, "-")
|
||||
hasUnderscore := strings.Contains(field, "_")
|
||||
|
||||
if hasHyphen {
|
||||
// provide variations of hyphen candidates with an underscore
|
||||
newValue := strings.ReplaceAll(field, "-", "_")
|
||||
underscoreCandidate := candidate
|
||||
underscoreCandidate.value = newValue
|
||||
fields.add(underscoreCandidate)
|
||||
}
|
||||
|
||||
if hasUnderscore {
|
||||
// provide variations of underscore candidates with a hyphen
|
||||
newValue := strings.ReplaceAll(field, "_", "-")
|
||||
hyphenCandidate := candidate
|
||||
hyphenCandidate.value = newValue
|
||||
fields.add(hyphenCandidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// candidateProductForGo attempts to find a single product name in a best-effort attempt. This implementation prefers
|
||||
// to return no vendor over returning potentially nonsensical results.
|
||||
func candidateProductForGo(name string) string {
|
||||
// note: url.Parse requires a scheme for correct processing, which a golang module will not have, so one is provided.
|
||||
u, err := url.Parse("http://" + name)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
cleanPath := strings.Trim(u.Path, "/")
|
||||
pathElements := strings.Split(cleanPath, "/")
|
||||
|
||||
switch u.Host {
|
||||
case "golang.org", "gopkg.in":
|
||||
return cleanPath
|
||||
case "google.golang.org":
|
||||
return pathElements[0]
|
||||
}
|
||||
|
||||
if len(pathElements) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// returning the rest of the path here means longer CPEs, it helps avoiding false-positives
|
||||
// ref: https://github.com/anchore/grype/issues/676
|
||||
return strings.Join(pathElements[1:], "/")
|
||||
}
|
||||
|
||||
// candidateVendorForGo attempts to find a single vendor name in a best-effort attempt. This implementation prefers
|
||||
// to return no vendor over returning potentially nonsensical results.
|
||||
func candidateVendorForGo(name string) string {
|
||||
// note: url.Parse requires a scheme for correct processing, which a golang module will not have, so one is provided.
|
||||
u, err := url.Parse("http://" + name)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
cleanPath := strings.Trim(u.Path, "/")
|
||||
|
||||
switch u.Host {
|
||||
case "google.golang.org":
|
||||
return "google"
|
||||
case "golang.org":
|
||||
return "golang"
|
||||
case "gopkg.in":
|
||||
return ""
|
||||
}
|
||||
|
||||
pathElements := strings.Split(cleanPath, "/")
|
||||
if len(pathElements) < 2 {
|
||||
return ""
|
||||
}
|
||||
return pathElements[0]
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCandidateProductForGo(t *testing.T) {
|
||||
tests := []struct {
|
||||
pkg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
pkg: "github.com/someone/something",
|
||||
expected: "something",
|
||||
},
|
||||
{
|
||||
pkg: "golang.org/x/xerrors",
|
||||
expected: "x/xerrors",
|
||||
},
|
||||
{
|
||||
pkg: "gopkg.in/yaml.v2",
|
||||
expected: "yaml.v2",
|
||||
},
|
||||
{
|
||||
pkg: "place",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "place.com/",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "place.com/someone-or-thing",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "google.golang.org/genproto/googleapis/rpc/status",
|
||||
expected: "genproto",
|
||||
},
|
||||
{
|
||||
pkg: "github.com/someone/something/long/package/name",
|
||||
expected: "something/long/package/name",
|
||||
},
|
||||
{
|
||||
pkg: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.pkg, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, candidateProductForGo(test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateVendorForGo(t *testing.T) {
|
||||
tests := []struct {
|
||||
pkg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
pkg: "github.com/someone/something",
|
||||
expected: "someone",
|
||||
},
|
||||
{
|
||||
pkg: "golang.org/x/xerrors",
|
||||
expected: "golang",
|
||||
},
|
||||
{
|
||||
pkg: "gopkg.in/yaml.v2",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "place",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "place.com/",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "place.com/someone-or-thing",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
pkg: "google.golang.org/genproto/googleapis/rpc/status",
|
||||
expected: "google",
|
||||
},
|
||||
{
|
||||
pkg: "github.com/someone/something/long/package/name",
|
||||
expected: "someone",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.pkg, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, candidateVendorForGo(test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
forbiddenProductGroupIDFields = strset.New("plugin", "plugins", "client")
|
||||
forbiddenVendorGroupIDFields = strset.New("plugin", "plugins")
|
||||
|
||||
domains = []string{
|
||||
"com",
|
||||
"org",
|
||||
"net",
|
||||
"io",
|
||||
"be",
|
||||
}
|
||||
|
||||
PrimaryJavaManifestGroupIDFields = []string{
|
||||
"Bundle-SymbolicName",
|
||||
"Extension-Name",
|
||||
"Specification-Vendor",
|
||||
"Implementation-Vendor",
|
||||
"Implementation-Vendor-Id",
|
||||
"Implementation-Title",
|
||||
"Bundle-Activator",
|
||||
}
|
||||
SecondaryJavaManifestGroupIDFields = []string{
|
||||
"Automatic-Module-Name",
|
||||
"Main-Class",
|
||||
"Package",
|
||||
}
|
||||
javaManifestNameFields = []string{
|
||||
"Specification-Vendor",
|
||||
"Implementation-Vendor",
|
||||
}
|
||||
)
|
||||
|
||||
func candidateProductsForJava(p pkg.Package) []string {
|
||||
return productsFromArtifactAndGroupIDs(artifactIDFromJavaPackage(p), GroupIDsFromJavaPackage(p))
|
||||
}
|
||||
|
||||
func candidateVendorsForJava(p pkg.Package) fieldCandidateSet {
|
||||
gidVendors := vendorsFromGroupIDs(GroupIDsFromJavaPackage(p))
|
||||
nameVendors := vendorsFromJavaManifestNames(p)
|
||||
return newFieldCandidateSetFromSets(gidVendors, nameVendors)
|
||||
}
|
||||
|
||||
func vendorsFromJavaManifestNames(p pkg.Package) fieldCandidateSet {
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
metadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
if !ok {
|
||||
return vendors
|
||||
}
|
||||
|
||||
if metadata.Manifest == nil {
|
||||
return vendors
|
||||
}
|
||||
|
||||
for _, name := range javaManifestNameFields {
|
||||
if metadata.Manifest.Main != nil {
|
||||
if value, exists := metadata.Manifest.Main[name]; exists {
|
||||
if !startsWithTopLevelDomain(value) {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizePersonName(value),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if metadata.Manifest.NamedSections != nil {
|
||||
for _, section := range metadata.Manifest.NamedSections {
|
||||
if section == nil {
|
||||
continue
|
||||
}
|
||||
if value, exists := section[name]; exists {
|
||||
if !startsWithTopLevelDomain(value) {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizePersonName(value),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func vendorsFromGroupIDs(groupIDs []string) fieldCandidateSet {
|
||||
vendors := newFieldCandidateSet()
|
||||
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
|
||||
}
|
||||
|
||||
vendors.addValue(field)
|
||||
}
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
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 expect
|
||||
// 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.JavaArchive)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if metadata.PomProperties == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
artifactID := strings.TrimSpace(metadata.PomProperties.ArtifactID)
|
||||
if looksLikeGroupID(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 GroupIDsFromJavaPackage(p pkg.Package) (groupIDs []string) {
|
||||
metadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return GroupIDsFromJavaMetadata(p.Name, metadata)
|
||||
}
|
||||
|
||||
// GroupIDsFromJavaMetadata returns the possible group IDs for a Java package
|
||||
// This function is similar to GroupIDFromJavaPackage, but returns all possible group IDs and is less strict
|
||||
// It is used as a way to generate possible candidates for CPE matching.
|
||||
func GroupIDsFromJavaMetadata(pkgName string, metadata pkg.JavaArchive) (groupIDs []string) {
|
||||
groupIDs = append(groupIDs, groupIDsFromPomProperties(metadata.PomProperties)...)
|
||||
groupIDs = append(groupIDs, groupIDsFromPomProject(metadata.PomProject)...)
|
||||
groupIDs = append(groupIDs, groupIDsFromJavaManifest(pkgName, metadata.Manifest)...)
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func groupIDsFromPomProperties(properties *pkg.JavaPomProperties) (groupIDs []string) {
|
||||
if properties == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if startsWithTopLevelDomain(properties.GroupID) {
|
||||
groupIDs = append(groupIDs, cleanGroupID(properties.GroupID))
|
||||
}
|
||||
|
||||
// sometimes the publisher puts the group ID in the artifact ID field unintentionally
|
||||
if startsWithTopLevelDomain(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, cleanGroupID(properties.ArtifactID))
|
||||
}
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func groupIDsFromPomProject(project *pkg.JavaPomProject) (groupIDs []string) {
|
||||
if project == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract the project info...
|
||||
groupIDs = addGroupIDsFromGroupIDsAndArtifactID(project.GroupID, project.ArtifactID)
|
||||
|
||||
if project.Parent == nil {
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
// extract the parent project info...
|
||||
groupIDs = append(groupIDs, addGroupIDsFromGroupIDsAndArtifactID(project.Parent.GroupID, project.Parent.ArtifactID)...)
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func addGroupIDsFromGroupIDsAndArtifactID(groupID, artifactID string) (groupIDs []string) {
|
||||
if startsWithTopLevelDomain(groupID) {
|
||||
groupIDs = append(groupIDs, cleanGroupID(groupID))
|
||||
}
|
||||
|
||||
// sometimes the publisher puts the group ID in the artifact ID field unintentionally
|
||||
if startsWithTopLevelDomain(artifactID) && len(strings.Split(artifactID, ".")) > 1 {
|
||||
// there is a strong indication that the artifact ID is really a group ID
|
||||
groupIDs = append(groupIDs, cleanGroupID(artifactID))
|
||||
}
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func groupIDsFromJavaManifest(pkgName string, manifest *pkg.JavaManifest) []string {
|
||||
if groupID, ok := DefaultArtifactIDToGroupID[pkgName]; ok {
|
||||
return []string{groupID}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// try the common manifest fields first for a set of candidates
|
||||
groupIDs := GetManifestFieldGroupIDs(manifest, PrimaryJavaManifestGroupIDFields)
|
||||
|
||||
if len(groupIDs) != 0 {
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
// if we haven't found anything yet, let's try a last ditch effort:
|
||||
// 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.
|
||||
return GetManifestFieldGroupIDs(manifest, SecondaryJavaManifestGroupIDFields)
|
||||
}
|
||||
|
||||
func GetManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (groupIDs []string) {
|
||||
if manifest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, name := range fields {
|
||||
if value, exists := manifest.Main[name]; exists {
|
||||
if startsWithTopLevelDomain(value) {
|
||||
groupIDs = append(groupIDs, cleanGroupID(value))
|
||||
}
|
||||
}
|
||||
for _, section := range manifest.NamedSections {
|
||||
if value, exists := section[name]; exists {
|
||||
if startsWithTopLevelDomain(value) {
|
||||
groupIDs = append(groupIDs, cleanGroupID(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(groupIDs)
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func cleanGroupID(groupID string) string {
|
||||
return strings.TrimSpace(strings.Split(removeOSCIDirectives(groupID), "#")[0])
|
||||
}
|
||||
|
||||
func removeOSCIDirectives(groupID string) string {
|
||||
// for example:
|
||||
// org.bar;uses:=“org.foo” -> org.bar
|
||||
// more about OSGI directives see https://spring.io/blog/2008/10/20/understanding-the-osgi-uses-directive/
|
||||
return strings.Split(groupID, ";")[0]
|
||||
}
|
||||
|
||||
func startsWithTopLevelDomain(value string) bool {
|
||||
return internal.HasAnyOfPrefixes(value, domains...)
|
||||
}
|
||||
|
||||
func looksLikeGroupID(value string) bool {
|
||||
return strings.Contains(value, ".")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,476 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
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.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "org.sonatype.nexus",
|
||||
ArtifactID: "org.sonatype.nexus",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"nexus"},
|
||||
},
|
||||
{
|
||||
name: "detect groupID-like value in artifactID field",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
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 {
|
||||
groupID string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
groupID: "org.sonatype.nexus",
|
||||
expected: []string{"sonatype", "nexus"},
|
||||
},
|
||||
{
|
||||
groupID: "org.jenkins-ci.plugins",
|
||||
expected: []string{"jenkins-ci"},
|
||||
},
|
||||
{
|
||||
groupID: "io.jenkins.plugins",
|
||||
expected: []string{"jenkins"},
|
||||
},
|
||||
{
|
||||
groupID: "com.cloudbees.jenkins.plugins",
|
||||
expected: []string{"cloudbees", "jenkins"},
|
||||
},
|
||||
{
|
||||
groupID: "com.atlassian.confluence.plugins",
|
||||
expected: []string{"atlassian", "confluence"},
|
||||
},
|
||||
{
|
||||
groupID: "com.google.guava",
|
||||
expected: []string{"google", "guava"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.groupID, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, vendorsFromGroupIDs([]string{test.groupID}).values(), "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.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "io.jenkins-ci.plugin.thing;version='[2,3)'",
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{"io.jenkins-ci.plugin.thing"},
|
||||
},
|
||||
{
|
||||
name: "clean # suffixes",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "org.elasticsearch.plugin#parent-join;6.8.15",
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{"org.elasticsearch.plugin"},
|
||||
},
|
||||
{
|
||||
name: "from artifactID",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
ArtifactID: "io.jenkins-ci.plugin.thing; version='[2,3)' ; org.something.else",
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{"io.jenkins-ci.plugin.thing"},
|
||||
},
|
||||
{
|
||||
name: "from main Extension-Name field",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
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.JavaArchive{
|
||||
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 field - tier 1",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
// positive cases
|
||||
// tier 1
|
||||
"Extension-Name": "io.jenkins-ci.plugin.1",
|
||||
"Specification-Vendor": "io.jenkins-ci.plugin.2",
|
||||
"Implementation-Vendor": "io.jenkins-ci.plugin.3",
|
||||
"Bundle-SymbolicName": "io.jenkins-ci.plugin.4",
|
||||
"Implementation-Vendor-Id": "io.jenkins-ci.plugin.5",
|
||||
"Implementation-Title": "io.jenkins-ci.plugin.6",
|
||||
"Bundle-Activator": "io.jenkins-ci.plugin.7",
|
||||
// tier 2
|
||||
"Automatic-Module-Name": "io.jenkins-ci.plugin.8",
|
||||
"Main-Class": "io.jenkins-ci.plugin.9",
|
||||
"Package": "io.jenkins-ci.plugin.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{
|
||||
"io.jenkins-ci.plugin.1",
|
||||
"io.jenkins-ci.plugin.2",
|
||||
"io.jenkins-ci.plugin.3",
|
||||
"io.jenkins-ci.plugin.4",
|
||||
"io.jenkins-ci.plugin.5",
|
||||
"io.jenkins-ci.plugin.6",
|
||||
"io.jenkins-ci.plugin.7",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from main field - tier 2",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
// positive cases
|
||||
"Automatic-Module-Name": "io.jenkins-ci.plugin.8",
|
||||
"Main-Class": "io.jenkins-ci.plugin.9",
|
||||
"Package": "io.jenkins-ci.plugin.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{
|
||||
"io.jenkins-ci.plugin.8",
|
||||
"io.jenkins-ci.plugin.9",
|
||||
"io.jenkins-ci.plugin.10",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from main field - negative cases",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
// negative cases
|
||||
"Extension-Name": "not.a-group.id",
|
||||
"bogus": "io.jenkins-ci.plugin.please-dont-find-me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: nil,
|
||||
},
|
||||
{
|
||||
name: "from named section field - tier 1",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
NamedSections: map[string]map[string]string{
|
||||
"section": {
|
||||
// positive cases
|
||||
// tier 1
|
||||
"Extension-Name": "io.jenkins-ci.plugin.1",
|
||||
"Specification-Vendor": "io.jenkins-ci.plugin.2",
|
||||
"Implementation-Vendor": "io.jenkins-ci.plugin.3",
|
||||
"Bundle-SymbolicName": "io.jenkins-ci.plugin.4",
|
||||
"Implementation-Vendor-Id": "io.jenkins-ci.plugin.5",
|
||||
"Implementation-Title": "io.jenkins-ci.plugin.6",
|
||||
"Bundle-Activator": "io.jenkins-ci.plugin.7",
|
||||
// tier 2
|
||||
"Automatic-Module-Name": "io.jenkins-ci.plugin.8",
|
||||
"Main-Class": "io.jenkins-ci.plugin.9",
|
||||
"Package": "io.jenkins-ci.plugin.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{
|
||||
"io.jenkins-ci.plugin.1",
|
||||
"io.jenkins-ci.plugin.2",
|
||||
"io.jenkins-ci.plugin.3",
|
||||
"io.jenkins-ci.plugin.4",
|
||||
"io.jenkins-ci.plugin.5",
|
||||
"io.jenkins-ci.plugin.6",
|
||||
"io.jenkins-ci.plugin.7",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from named section field - negative cases",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
NamedSections: map[string]map[string]string{
|
||||
"section": {
|
||||
// negative cases
|
||||
"Extension-Name": "not.a-group.id",
|
||||
"bogus": "io.jenkins-ci.plugin.please-dont-find-me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: nil,
|
||||
},
|
||||
{
|
||||
name: "no manifest or pom info",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{},
|
||||
},
|
||||
expects: nil,
|
||||
},
|
||||
{
|
||||
name: "no java info",
|
||||
pkg: pkg.Package{},
|
||||
expects: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(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.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
ArtifactID: "cloudbees-installation-manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: "cloudbees-installation-manager",
|
||||
},
|
||||
{
|
||||
name: "ignore groupID-like things",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_vendorsFromJavaManifestNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expects []string
|
||||
}{
|
||||
{
|
||||
name: "from manifest named section fields",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
NamedSections: map[string]map[string]string{
|
||||
"section": {
|
||||
// positive cases
|
||||
"Specification-Vendor": "Alex Goodman",
|
||||
"Implementation-Vendor": "William Goodman",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{"alex_goodman", "william_goodman"},
|
||||
},
|
||||
{
|
||||
name: "from manifest named section fields - negative cases",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
NamedSections: map[string]map[string]string{
|
||||
"section": {
|
||||
// negative cases
|
||||
"Specification-Vendor": "io.jenkins-ci.plugin.thing",
|
||||
"Implementation-Vendor-ID": "William Goodman",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expects, vendorsFromJavaManifestNames(test.pkg).values())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_groupIDsFromJavaManifest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest pkg.JavaManifest
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "spring-security-core",
|
||||
manifest: pkg.JavaManifest{},
|
||||
expected: []string{"org.springframework.security"},
|
||||
},
|
||||
{
|
||||
name: "spring-web",
|
||||
manifest: pkg.JavaManifest{},
|
||||
expected: []string{"org.springframework"},
|
||||
},
|
||||
{
|
||||
name: "spring-foo",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Implementation-Vendor": "org.foo",
|
||||
},
|
||||
},
|
||||
expected: []string{"org.foo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := groupIDsFromJavaManifest(test.name, &test.manifest)
|
||||
require.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func candidateVendorsForJavascript(p pkg.Package) fieldCandidateSet {
|
||||
if _, ok := p.Metadata.(pkg.NpmPackage); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
metadata, ok := p.Metadata.(pkg.NpmPackage)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if metadata.URL != "" {
|
||||
vendors.union(candidateVendorsFromURL(metadata.URL))
|
||||
}
|
||||
|
||||
if metadata.Homepage != "" {
|
||||
vendors.union(candidateVendorsFromURL(metadata.Homepage))
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func additionalVendorsForPython(v string) (vendors []string) {
|
||||
if !strings.HasSuffix(v, "project") {
|
||||
vendors = append(vendors, fmt.Sprintf("%sproject", v), fmt.Sprintf("%s_project", v))
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func candidateVendorsForPython(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.PythonPackage)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
if metadata.Author != "" {
|
||||
name := normalizePersonName(metadata.Author)
|
||||
vendors.add(fieldCandidate{
|
||||
value: name,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
|
||||
for _, v := range additionalVendorsForPython(name) {
|
||||
vendors.add(fieldCandidate{
|
||||
value: v,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if metadata.AuthorEmail != "" {
|
||||
name := normalizePersonName(stripEmailSuffix(metadata.AuthorEmail))
|
||||
vendors.add(fieldCandidate{
|
||||
value: name,
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
|
||||
for _, v := range additionalVendorsForPython(name) {
|
||||
vendors.add(fieldCandidate{
|
||||
value: v,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func candidateVendorsForRPM(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.RpmDBEntry)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
if metadata.Vendor != "" {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizeName(metadata.Vendor),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func candidateVendorsForRuby(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.RubyGemspec)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
for _, author := range metadata.Authors {
|
||||
// author could be a name or an email
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizePersonName(stripEmailSuffix(author)),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
|
||||
if metadata.Homepage != "" {
|
||||
vendors.union(candidateVendorsFromURL(metadata.Homepage))
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import "strings"
|
||||
|
||||
func stripEmailSuffix(email string) string {
|
||||
return strings.Split(email, "@")[0]
|
||||
}
|
||||
|
||||
func normalizePersonName(name string) string {
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
for _, value := range []string{"-", " ", "."} {
|
||||
name = strings.ReplaceAll(name, value, "_")
|
||||
}
|
||||
return strings.TrimPrefix(name, "the_")
|
||||
}
|
||||
|
||||
func normalizeName(name string) string {
|
||||
name = strings.Split(name, ",")[0]
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
return strings.ReplaceAll(name, " ", "")
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_normalizeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expects string
|
||||
}{
|
||||
{
|
||||
// note: extra spaces
|
||||
input: " Alex Goodman ",
|
||||
expects: "alexgoodman",
|
||||
},
|
||||
{
|
||||
input: "Alex Goodman, LLC",
|
||||
expects: "alexgoodman",
|
||||
},
|
||||
{
|
||||
input: "alex.goodman",
|
||||
expects: "alex.goodman",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
assert.Equal(t, test.expects, normalizeName(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_normalizePersonName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expects string
|
||||
}{
|
||||
{
|
||||
// note: extra spaces
|
||||
input: " Alex Goodman ",
|
||||
expects: "alex_goodman",
|
||||
},
|
||||
{
|
||||
input: "Alex Goodman",
|
||||
expects: "alex_goodman",
|
||||
},
|
||||
{
|
||||
input: "Alex.Goodman",
|
||||
expects: "alex_goodman",
|
||||
},
|
||||
{
|
||||
input: "Alex.Goodman",
|
||||
expects: "alex_goodman",
|
||||
},
|
||||
{
|
||||
input: "AlexGoodman",
|
||||
expects: "alexgoodman",
|
||||
},
|
||||
{
|
||||
input: "The Apache Software Foundation",
|
||||
expects: "apache_software_foundation",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
assert.Equal(t, test.expects, normalizePersonName(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
urlPrefixToVendors = map[string][]string{
|
||||
"https://www.gnu.org/": {"gnu"},
|
||||
"https://developer.gnome.org/": {"gnome"},
|
||||
"https://www.ruby-lang.org/": {"ruby-lang"},
|
||||
"https://llvm.org/": {"llvm"},
|
||||
"https://www.isc.org/": {"isc"},
|
||||
"https://musl.libc.org/": {"musl-libc"},
|
||||
"https://www.mozilla.org/": {"mozilla"},
|
||||
"https://www.x.org/": {"x.org"},
|
||||
"https://w1.fi/": {"w1.fi"},
|
||||
}
|
||||
|
||||
vendorExtractionPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`^(?:https|http|git)://(?:github|gitlab)\.com/(?P<vendor>[\w\-]*?)/.*$`),
|
||||
}
|
||||
)
|
||||
|
||||
func candidateVendorsFromURL(url string) fieldCandidateSet {
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
for urlPrefix, additionalVendors := range urlPrefixToVendors {
|
||||
if strings.HasPrefix(url, urlPrefix) {
|
||||
for _, v := range additionalVendors {
|
||||
vendors.add(fieldCandidate{
|
||||
value: v,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
|
||||
return vendors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range vendorExtractionPatterns {
|
||||
groups := internal.MatchNamedCaptureGroups(p, url)
|
||||
|
||||
if v, ok := groups["vendor"]; ok {
|
||||
vendors.add(fieldCandidate{
|
||||
value: v,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
|
||||
return vendors
|
||||
}
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_candidateVendorsFromURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
url: "",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no known vendors",
|
||||
url: "https://something-unknown.com/126374623876/12345",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "gnu vendor from url",
|
||||
url: "https://www.gnu.org/software/make",
|
||||
expected: []string{"gnu"},
|
||||
},
|
||||
{
|
||||
name: "github username as vendor",
|
||||
url: "https://github.com/armadillo/abcxyz-12345",
|
||||
expected: []string{"armadillo"},
|
||||
},
|
||||
{
|
||||
name: "github username with - as vendor",
|
||||
url: "https://github.com/1234-abc-xyz/hello",
|
||||
expected: []string{"1234-abc-xyz"},
|
||||
},
|
||||
{
|
||||
name: "gitlab username as vendor",
|
||||
url: "https://gitlab.com/armadillo/abcxyz-12345",
|
||||
expected: []string{"armadillo"},
|
||||
},
|
||||
{
|
||||
name: "gitlab username with - as vendor",
|
||||
url: "https://gitlab.com/1234-abc-xyz/hello",
|
||||
expected: []string{"1234-abc-xyz"},
|
||||
},
|
||||
{
|
||||
name: "github username as vendor from longer url",
|
||||
url: "https://github.com/armadillo/abcxyz-12345/a/b/c/d/e/f/g",
|
||||
expected: []string{"armadillo"},
|
||||
},
|
||||
{
|
||||
name: "github username from git://",
|
||||
url: "git://github.com/abc/xyz.git",
|
||||
expected: []string{"abc"},
|
||||
},
|
||||
{
|
||||
name: "github username from http://",
|
||||
url: "http://github.com/abc/xyz.git",
|
||||
expected: []string{"abc"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, candidateVendorsFromURL(test.url).uniqueValues(), "different vendors")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user