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:
Alex Goodman
2024-01-26 12:16:05 -05:00
committed by GitHub
parent 7f90b8f1eb
commit b6cbf82389
36 changed files with 63 additions and 49 deletions
-131
View File
@@ -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
}
-440
View File
@@ -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))
})
}
}
+15
View File
@@ -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"
@@ -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:*:*"
}
}
}
@@ -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)
}
-75
View File
@@ -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))
})
}
}
-350
View File
@@ -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
-61
View File
@@ -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]
}
-103
View File
@@ -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))
})
}
}
-313
View File
@@ -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
-476
View File
@@ -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
}
-60
View File
@@ -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
}
-21
View File
@@ -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
}
-26
View File
@@ -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
}
-21
View File
@@ -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")
})
}
}