incorporate multiple match details to accomodate more accurate reported CPE matching info

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2021-05-30 11:46:16 -04:00
parent 06dcd811d9
commit ed054f2038
No known key found for this signature in database
GPG key ID: 5CB45AE22BAB7EA7
39 changed files with 1302 additions and 527 deletions

3
go.sum
View file

@ -114,7 +114,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alicebob/sqlittle v1.4.0 h1:vgYt0nAjhdf/hg52MjKJ84g/uTzBPfrvI+VUBrIghxA=
github.com/alicebob/sqlittle v1.4.0/go.mod h1:Co1L1qxHqCwf41puWhk2HOodojR0mcsAV4BIt8byZh8=
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6 h1:wEN3HXc3VuC4wo7Cz27YCpeQ4gaB5ASKwMwM5GdFsew=
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6/go.mod h1:8jNYOxCJC5kyD/Ct4MbzsDN2hOhRoCAzQcb/7KdYYGw=
@ -132,7 +131,6 @@ github.com/anchore/syft v0.16.2-0.20210526131825-2754c889eb0e/go.mod h1:9jf8z5ub
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
@ -651,7 +649,6 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View file

@ -2,6 +2,7 @@ package match
import (
"fmt"
"strings"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
@ -10,20 +11,41 @@ import (
// Match represents a finding in the vulnerability matching process, pairing a single package and a single vulnerability object.
type Match struct {
Type Type // The kind of match made (an exact match, fuzzy match, indirect vs direct, etc).
Confidence float64 // The certainty of the match as a ratio (currently unused, reserved for future use).
Vulnerability vulnerability.Vulnerability // The vulnerability details of the match.
Package pkg.Package // The package used to search for a match.
SearchedBy map[string]interface{} // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made.
SearchMatches map[string]interface{} // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within.
Matcher MatcherType // The matcher object that discovered the match.
MatchDetails []Details // all ways in which how this particular match was made.
}
type Details struct {
SearchedBy interface{} // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made.
MatchedOn interface{} // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within.
Matcher MatcherType // The matcher object that discovered the match.
Confidence float64 // The certainty of the match as a ratio (currently unused, reserved for future use).
}
type Fingerprint struct {
VulnerabilityID string
VulnerabilityNamespace string
VulnerabilityFixes string
PackageID pkg.ID // this encodes package name, version, type, location
MatchType Type
}
// String is the string representation of select match fields.
func (m Match) String() string {
return fmt.Sprintf("Match(pkg=%s vuln=%s type='%s' foundBy='%s')", m.Package, m.Vulnerability.String(), m.Type, m.Matcher)
return fmt.Sprintf("Match(pkg=%s vuln=%q type=%q)", m.Package, m.Vulnerability.String(), m.Type)
}
// Summary is a short string representation of the match object.
func (m Match) Summary() string {
return fmt.Sprintf("vuln='%s' type='%s' key='%s' foundBy='%s'", m.Vulnerability.ID, m.Type, m.SearchedBy, m.Matcher)
return fmt.Sprintf("vuln=%q type=%q searchedBy=%q foundBy=%q", m.Vulnerability.ID, m.Type, m.MatchDetails[0].SearchedBy, m.MatchDetails[0].Matcher)
}
func (m Match) Fingerprint() Fingerprint {
return Fingerprint{
VulnerabilityID: m.Vulnerability.ID,
VulnerabilityNamespace: m.Vulnerability.Namespace,
VulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","),
PackageID: m.Package.ID,
MatchType: m.Type,
}
}

View file

@ -44,7 +44,7 @@ func (r *Matches) add(id pkg.ID, matches ...Match) {
}
func (r *Matches) Add(p pkg.Package, matches ...Match) {
r.add(p.ID(), matches...)
r.add(p.ID, matches...)
}
func (r *Matches) Enumerate() <-chan Match {
@ -66,18 +66,7 @@ func (r *Matches) Sorted() []Match {
matches = append(matches, m)
}
sort.SliceStable(matches, func(i, j int) bool {
if matches[i].Vulnerability.ID == matches[j].Vulnerability.ID {
if matches[i].Package.Name == matches[j].Package.Name {
if matches[i].Package.Version == matches[j].Package.Version {
return matches[i].Package.Type < matches[j].Package.Type
}
return matches[i].Package.Version < matches[j].Package.Version
}
return matches[i].Package.Name < matches[j].Package.Name
}
return matches[i].Vulnerability.ID < matches[j].Vulnerability.ID
})
sort.Sort(ByElements(matches))
return matches
}

31
grype/match/sort.go Normal file
View file

@ -0,0 +1,31 @@
package match
import "sort"
var _ sort.Interface = (*ByElements)(nil)
type ByElements []Match
// Len is the number of elements in the collection.
func (m ByElements) Len() int {
return len(m)
}
// Less reports whether the element with index i should sort before the element with index j.
func (m ByElements) Less(i, j int) bool {
if m[i].Vulnerability.ID == m[j].Vulnerability.ID {
if m[i].Package.Name == m[j].Package.Name {
if m[i].Package.Version == m[j].Package.Version {
return m[i].Package.Type < m[j].Package.Type
}
return m[i].Package.Version < m[j].Package.Version
}
return m[i].Package.Name < m[j].Package.Name
}
return m[i].Vulnerability.ID < m[j].Vulnerability.ID
}
// Swap swaps the elements with indexes i and j.
func (m ByElements) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}

View file

@ -127,12 +127,9 @@ func matchesByID(matches []match.Match) map[string][]match.Match {
return results
}
func vulnerabilitiesByID(vulns []*vulnerability.Vulnerability) map[string][]*vulnerability.Vulnerability {
var results = make(map[string][]*vulnerability.Vulnerability)
func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulnerability.Vulnerability {
var results = make(map[string][]vulnerability.Vulnerability)
for _, vuln := range vulns {
if vuln == nil {
continue
}
results[vuln.ID] = append(results[vuln.ID], vuln)
}

View file

@ -3,6 +3,8 @@ package apk
import (
"testing"
"github.com/anchore/grype/grype/matcher/common"
"github.com/go-test/deep"
"github.com/anchore/grype/grype/match"
@ -72,20 +74,24 @@ func TestSecDBOnlyMatch(t *testing.T) {
expected := []match.Match{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnFound,
Package: p,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
},
"namespace": "secdb",
},
MatchedOn: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
},
Matcher: match.ApkMatcher,
},
},
SearchMatches: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"namespace": "secdb",
},
Matcher: match.ApkMatcher,
},
}
@ -148,20 +154,24 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
expected := []match.Match{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnFound,
Package: p,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
},
"namespace": "secdb",
},
MatchedOn: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
},
Matcher: match.ApkMatcher,
},
},
SearchMatches: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"namespace": "secdb",
},
Matcher: match.ApkMatcher,
},
}
@ -225,20 +235,24 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
expected := []match.Match{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnFound,
Package: p,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
},
"namespace": "secdb",
},
MatchedOn: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
},
Matcher: match.ApkMatcher,
},
},
SearchMatches: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"namespace": "secdb",
},
Matcher: match.ApkMatcher,
},
}
@ -288,18 +302,22 @@ func TestNvdOnlyMatches(t *testing.T) {
expected := []match.Match{
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Vulnerability: *vulnFound,
Package: p,
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: common.CPESearchInput{
CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
Namespace: "nvd",
},
MatchedOn: common.CPESearchHit{
CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
VersionConstraint: vulnFound.Constraint.String(),
},
Matcher: match.ApkMatcher,
},
},
SearchMatches: map[string]interface{}{
"cpes": []string{vulnFound.CPEs[0].BindToFmtString()},
"versionConstraint": vulnFound.Constraint.String(),
"namespace": "nvd",
},
Matcher: match.ApkMatcher,
},
}

View file

@ -2,79 +2,206 @@ package common
import (
"fmt"
"sort"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
"github.com/scylladb/go-set/strset"
)
type CPESearchInput struct {
Namespace string `json:"namespace"`
CPEs []string `json:"cpes"`
}
func (i *CPESearchInput) Merge(other CPESearchInput) error {
if i.Namespace != other.Namespace {
return fmt.Errorf("namespaces do not match")
}
existingCPEs := strset.New(i.CPEs...)
newCPEs := strset.New(other.CPEs...)
mergedCPEs := strset.Union(existingCPEs, newCPEs).List()
sort.Strings(mergedCPEs)
i.CPEs = mergedCPEs
return nil
}
type CPESearchHit struct {
VersionConstraint string `json:"versionConstraint"`
CPEs []string `json:"cpes"`
}
func (h CPESearchHit) Equals(other CPESearchHit) bool {
if h.VersionConstraint != other.VersionConstraint {
return false
}
if len(h.CPEs) != len(other.CPEs) {
return false
}
for i := range h.CPEs {
if h.CPEs[i] != other.CPEs[i] {
return false
}
}
return true
}
// FindMatchesByPackageCPE retrieves all vulnerabilities that match the generated CPE
func FindMatchesByPackageCPE(store vulnerability.ProviderByCPE, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {
verObj, err := version.NewVersionFromPkg(p)
if err != nil {
return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err)
}
matchesByFingerprint := make(map[match.Fingerprint]match.Match)
for _, cpe := range p.CPEs {
// prefer the CPE version, but if npt specified use the package version
pkgVersion := cpe.Version
if pkgVersion == wfn.NA || pkgVersion == wfn.Any {
pkgVersion = p.Version
}
verObj, err := version.NewVersion(pkgVersion, version.FormatFromPkgType(p.Type))
if err != nil {
return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err)
}
matches := make([]match.Match, 0)
vulnSet := vulnerability.NewSet()
vulnerableKeys := internal.NewStringSet()
for _, cpe := range verObj.CPEs() {
// find all vulnerability records in the DB for the given CPE (not including version comparisons)
allPkgVulns, err := store.GetByCPE(cpe)
if err != nil {
return nil, fmt.Errorf("matcher failed to fetch by CPE pkg='%s': %w", p.Name, err)
}
// for each vulnerability record found, check the version constraint. If the constraint is satisfied
// relative to the current version information from the CPE (or the package) then the given package
// is vulnerable.
for _, vuln := range allPkgVulns {
if vulnSet.Contains(vuln) {
continue
}
vulnSet.Add(vuln)
// if the constraint it met, then the given package has the vulnerability
isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj)
if err != nil {
return nil, fmt.Errorf("cpe matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err)
}
if isPackageVulnerable {
// create a string key to ensure we aren't adding previously added matches
vulnerableKey := fmt.Sprintf("%s%s%s", vuln.ID, cpe.BindToFmtString(), vuln.Constraint.String())
if vulnerableKeys.Contains(vulnerableKey) {
continue
}
vulnerableKeys.Add(vulnerableKey)
matches = append(matches, match.Match{
Type: match.FuzzyMatch,
Confidence: 0.9, // TODO: this is hard coded for now
Vulnerability: *vuln,
Package: p,
Matcher: upstreamMatcher,
SearchedBy: map[string]interface{}{
"cpe": cpe.BindToFmtString(),
},
SearchMatches: map[string]interface{}{
"namespace": vuln.Namespace,
"cpes": cpesToString(vuln.CPEs),
"versionConstraint": vuln.Constraint.String(),
},
})
if !isPackageVulnerable {
continue
}
addNewMatch(matchesByFingerprint, vuln, p, verObj, upstreamMatcher, cpe)
}
}
return matches, err
return toMatches(matchesByFingerprint), nil
}
func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, pkgVersion *version.Version, upstreamMatcher match.MatcherType, searchedByCPE syftPkg.CPE) {
candidateMatch := match.Match{
Type: match.FuzzyMatch,
Vulnerability: vuln,
Package: p,
}
if existingMatch, exists := matchesByFingerprint[candidateMatch.Fingerprint()]; exists {
candidateMatch = existingMatch
}
candidateMatch.MatchDetails = addMatchDetails(candidateMatch.MatchDetails,
match.Details{
Confidence: 0.9, // TODO: this is hard coded for now
Matcher: upstreamMatcher,
SearchedBy: CPESearchInput{
Namespace: vuln.Namespace,
CPEs: []string{
searchedByCPE.BindToFmtString(),
},
},
MatchedOn: CPESearchHit{
VersionConstraint: vuln.Constraint.String(),
CPEs: cpesToString(keepMatchingCPEs(pkgVersion, vuln.CPEs)),
},
},
)
matchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch
}
func addMatchDetails(existingDetails []match.Details, newDetails match.Details) []match.Details {
newMatchedOn, ok := newDetails.MatchedOn.(CPESearchHit)
if !ok {
return existingDetails
}
newSearchedBy, ok := newDetails.SearchedBy.(CPESearchInput)
if !ok {
return existingDetails
}
for idx, detail := range existingDetails {
matchedOn, ok := detail.MatchedOn.(CPESearchHit)
if !ok {
continue
}
searchedBy, ok := detail.SearchedBy.(CPESearchInput)
if !ok {
continue
}
if !matchedOn.Equals(newMatchedOn) {
continue
}
err := searchedBy.Merge(newSearchedBy)
if err != nil {
continue
}
existingDetails[idx].SearchedBy = searchedBy
return existingDetails
}
// could not merge with another entry, append to the end
existingDetails = append(existingDetails, newDetails)
return existingDetails
}
func keepMatchingCPEs(pkgVersion *version.Version, allCPEs []syftPkg.CPE) (matchedCPEs []syftPkg.CPE) {
for _, c := range allCPEs {
if c.Version == wfn.Any || c.Version == wfn.NA {
matchedCPEs = append(matchedCPEs, c)
continue
}
constraint, err := version.GetConstraint(c.Version, version.UnknownFormat)
if err != nil {
// if we can't get a version constraint, don't filter out the CPE
matchedCPEs = append(matchedCPEs, c)
continue
}
satisfied, err := constraint.Satisfied(pkgVersion)
if err != nil || satisfied {
// if we can't check for version satisfaction, don't filter out the CPE
matchedCPEs = append(matchedCPEs, c)
continue
}
}
return matchedCPEs
}
func toMatches(matchesByFingerprint map[match.Fingerprint]match.Match) (matches []match.Match) {
for _, m := range matchesByFingerprint {
matches = append(matches, m)
}
sort.Sort(match.ByElements(matches))
return matches
}
// cpesToString receives one or more CPEs and stringifies them
func cpesToString(cpes []wfn.Attributes) []string {
var stringers = make([]string, 0)
for _, cpe := range cpes {
stringers = append(stringers, cpe.BindToFmtString())
func cpesToString(cpes []syftPkg.CPE) []string {
var strs = make([]string, len(cpes))
for idx, cpe := range cpes {
strs[idx] = cpe.BindToFmtString()
}
return stringers
sort.Strings(strs)
return strs
}

View file

@ -3,6 +3,8 @@ package common
import (
"testing"
"github.com/anchore/grype-db/pkg/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
@ -18,60 +20,77 @@ func must(c syftPkg.CPE, e error) syftPkg.CPE {
return c
}
type mockCPEProvider struct {
data map[string]map[string][]*vulnerability.Vulnerability
var _ db.VulnerabilityStoreReader = (*mockVulnStore)(nil)
type mockVulnStore struct {
data map[string]map[string][]db.Vulnerability
}
func newMockProviderByCPE() *mockCPEProvider {
pr := mockCPEProvider{
data: make(map[string]map[string][]*vulnerability.Vulnerability),
func newMockStore() *mockVulnStore {
pr := mockVulnStore{
data: make(map[string]map[string][]db.Vulnerability),
}
pr.stub()
return &pr
}
func (pr *mockCPEProvider) stub() {
pr.data["nvd"] = map[string][]*vulnerability.Vulnerability{
func (pr *mockVulnStore) stub() {
pr.data["nvd"] = map[string][]db.Vulnerability{
"activerecord": {
{
Constraint: version.MustGetConstraint("< 3.7.6", version.SemanticFormat),
ID: "CVE-2017-fake-1",
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*")),
PackageName: "activerecord",
VersionConstraint: "< 3.7.6",
VersionFormat: version.SemanticFormat.String(),
ID: "CVE-2017-fake-1",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
Namespace: "nvd",
},
{
Constraint: version.MustGetConstraint("< 3.7.4", version.SemanticFormat),
ID: "CVE-2017-fake-2",
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*")),
PackageName: "activerecord",
VersionConstraint: "< 3.7.4",
VersionFormat: version.SemanticFormat.String(),
ID: "CVE-2017-fake-2",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*",
},
Namespace: "nvd",
},
{
Constraint: version.MustGetConstraint("= 4.0.1", version.SemanticFormat),
ID: "CVE-2017-fake-3",
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*")),
},
Namespace: "nvd",
},
{
Constraint: version.MustGetConstraint("= 4.0.1", version.SemanticFormat),
ID: "CVE-2017-fake-3",
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*")),
PackageName: "activerecord",
VersionConstraint: "= 4.0.1",
VersionFormat: version.SemanticFormat.String(),
ID: "CVE-2017-fake-3",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*",
},
Namespace: "nvd",
},
},
"awesome": {
{
Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat),
ID: "CVE-2017-fake-4",
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*")),
PackageName: "awesome",
VersionConstraint: "< 98SP3",
VersionFormat: version.UnknownFormat.String(),
ID: "CVE-2017-fake-4",
CPEs: []string{
"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
},
Namespace: "nvd",
},
},
"multiple": {
{
PackageName: "multiple",
VersionConstraint: "< 4.0",
VersionFormat: version.UnknownFormat.String(),
ID: "CVE-2017-fake-5",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*",
},
Namespace: "nvd",
},
@ -79,8 +98,8 @@ func (pr *mockCPEProvider) stub() {
}
}
func (pr *mockCPEProvider) GetByCPE(c syftPkg.CPE) ([]*vulnerability.Vulnerability, error) {
return pr.data["nvd"][c.Product], nil
func (pr *mockVulnStore) GetVulnerability(namespace, pkg string) ([]db.Vulnerability, error) {
return pr.data[namespace][pkg], nil
}
func TestFindMatchesByPackageCPE(t *testing.T) {
@ -104,8 +123,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
},
expected: []match.Match{
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-1",
},
@ -119,15 +137,20 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:rando2:*:ruby:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:rando3:*:rails:*:*"},
},
MatchedOn: CPESearchHit{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
VersionConstraint: "< 3.7.6 (semver)",
},
Matcher: matcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "nvd",
"cpes": []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
"versionConstraint": "< 3.7.6 (semver)",
},
Matcher: matcher,
},
},
},
@ -145,8 +168,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
},
expected: []match.Match{
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-1",
},
@ -160,19 +182,26 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:rando2:*:ruby:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:rando3:*:rails:*:*",
},
Namespace: "nvd",
},
MatchedOn: CPESearchHit{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
VersionConstraint: "< 3.7.6 (semver)",
},
Matcher: matcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "nvd",
"cpes": []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
"versionConstraint": "< 3.7.6 (semver)",
},
Matcher: matcher,
},
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-2",
},
@ -186,15 +215,21 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:rando2:*:ruby:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:rando2:*:ruby:*:*"},
Namespace: "nvd",
},
MatchedOn: CPESearchHit{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"},
VersionConstraint: "< 3.7.4 (semver)",
},
Matcher: matcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "nvd",
"cpes": []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"},
"versionConstraint": "< 3.7.4 (semver)",
},
Matcher: matcher,
},
},
},
@ -202,8 +237,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
name: "exact match",
p: pkg.Package{
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:4.0.1:rando1:*:rando2:*:ruby:*:*")),
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:4.0.1:rando4:*:rando3:*:rails:*:*")),
must(syftPkg.NewCPE("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*")),
},
Name: "activerecord",
Version: "4.0.1",
@ -212,30 +246,33 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
},
expected: []match.Match{
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-3",
},
Package: pkg.Package{
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:4.0.1:rando1:*:rando2:*:ruby:*:*")),
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:4.0.1:rando4:*:rando3:*:rails:*:*")),
must(syftPkg.NewCPE("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*")),
},
Name: "activerecord",
Version: "4.0.1",
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:activerecord:activerecord:4.0.1:rando1:*:rando2:*:ruby:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"},
Namespace: "nvd",
},
MatchedOn: CPESearchHit{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"},
VersionConstraint: "= 4.0.1 (semver)",
},
Matcher: matcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "nvd",
"cpes": []string{"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*"},
"versionConstraint": "= 4.0.1 (semver)",
},
Matcher: matcher,
},
},
},
@ -260,8 +297,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
},
expected: []match.Match{
{
Type: match.FuzzyMatch,
Confidence: 0.9,
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-4",
},
@ -272,15 +308,68 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Name: "awesome",
Version: "98SE1",
},
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:awesome:awesome:98SE1:rando1:*:rando2:*:dunno:*:*",
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:rando2:*:dunno:*:*"},
Namespace: "nvd",
},
MatchedOn: CPESearchHit{
CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"},
VersionConstraint: "< 98SP3 (unknown)",
},
Matcher: matcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "nvd",
"cpes": []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"},
"versionConstraint": "< 98SP3 (unknown)",
},
},
},
{
name: "multiple matched CPEs",
p: pkg.Package{
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*")),
},
Name: "multiple",
Version: "1.0",
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
expected: []match.Match{
{
Type: match.FuzzyMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-5",
},
Package: pkg.Package{
CPEs: []syftPkg.CPE{
must(syftPkg.NewCPE("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*")),
},
Name: "multiple",
Version: "1.0",
Language: syftPkg.Ruby,
Type: syftPkg.GemPkg,
},
MatchDetails: []match.Details{
{
Confidence: 0.9,
SearchedBy: CPESearchInput{
CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"},
Namespace: "nvd",
},
MatchedOn: CPESearchHit{
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
VersionConstraint: "< 4.0 (unknown)",
},
Matcher: matcher,
},
},
Matcher: matcher,
},
},
},
@ -288,10 +377,422 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store := newMockProviderByCPE()
actual, err := FindMatchesByPackageCPE(store, test.p, matcher)
actual, err := FindMatchesByPackageCPE(vulnerability.NewProviderFromStore(newMockStore()), test.p, matcher)
assert.NoError(t, err)
assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)
for idx, e := range test.expected {
assert.Equal(t, e.MatchDetails, actual[idx].MatchDetails)
}
})
}
}
func TestKeepMatchingCPEs(t *testing.T) {
tests := []struct {
name string
version string
vulnerabilityCPEs []string
expected []string
}{
{
name: "filter out by simple version",
version: "1.0",
vulnerabilityCPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*",
},
expected: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// format strings to CPE objects...
vulnerabilityCPEs := make([]syftPkg.CPE, len(test.vulnerabilityCPEs))
for idx, c := range test.vulnerabilityCPEs {
vulnerabilityCPEs[idx] = must(syftPkg.NewCPE(c))
}
versionObj, err := version.NewVersion(test.version, version.UnknownFormat)
if err != nil {
t.Fatalf("unable to get version: %+v", err)
}
// run the test subject...
actual := keepMatchingCPEs(versionObj, vulnerabilityCPEs)
// format CPE objects to string...
actualStrs := make([]string, len(actual))
for idx, a := range actual {
actualStrs[idx] = a.BindToFmtString()
}
assert.ElementsMatch(t, test.expected, actualStrs)
})
}
}
func TestAddMatchDetails(t *testing.T) {
tests := []struct {
name string
existing []match.Details
new match.Details
expected []match.Details
}{
{
name: "append new entry -- matchedOn not equal",
existing: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
new: match.Details{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"totally-different-search",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"totally-different-match",
},
},
},
expected: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"totally-different-search",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"totally-different-match",
},
},
},
},
},
{
name: "append new entry -- searchedBy merge fails",
existing: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
new: match.Details{
SearchedBy: CPESearchInput{
Namespace: "totally-different",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
expected: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
{
SearchedBy: CPESearchInput{
Namespace: "totally-different",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
},
{
name: "merge with exiting entry",
existing: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
new: match.Details{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"totally-different-search",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
expected: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
"totally-different-search",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
},
{
name: "no addition - bad new searchedBy type",
existing: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
new: match.Details{
SearchedBy: "something else!",
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
expected: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
},
{
name: "no addition - bad new matchedOn type",
existing: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
new: match.Details{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: "something-else!",
},
expected: []match.Details{
{
SearchedBy: CPESearchInput{
Namespace: "nvd",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
},
MatchedOn: CPESearchHit{
VersionConstraint: "< 2.0 (unknown)",
CPEs: []string{
"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, addMatchDetails(test.existing, test.new))
})
}
}
func TestCPESearchHit_Equals(t *testing.T) {
tests := []struct {
name string
current CPESearchHit
other CPESearchHit
expected bool
}{
{
name: "different version constraint",
current: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
},
},
other: CPESearchHit{
VersionConstraint: "different-constraint",
CPEs: []string{
"a-cpe",
},
},
expected: false,
},
{
name: "different number of CPEs",
current: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
},
},
other: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
"b-cpe",
},
},
expected: false,
},
{
name: "different CPE value",
current: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
},
},
other: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"b-cpe",
},
},
expected: false,
},
{
name: "matches",
current: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
},
},
other: CPESearchHit{
VersionConstraint: "current-constraint",
CPEs: []string{
"a-cpe",
},
},
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, test.current.Equals(test.other))
})
}
}

View file

@ -20,7 +20,7 @@ func FindMatchesByPackageDistro(store vulnerability.ProviderByDistro, d *distro.
return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err)
}
var allPkgVulns []*vulnerability.Vulnerability
var allPkgVulns []vulnerability.Vulnerability
allPkgVulns, err = store.GetByDistro(*d, p)
if err != nil {
@ -35,25 +35,31 @@ func FindMatchesByPackageDistro(store vulnerability.ProviderByDistro, d *distro.
return nil, fmt.Errorf("distro matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err)
}
if isPackageVulnerable {
matches = append(matches, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0, // TODO: this is hard coded for now
Vulnerability: *vuln,
Package: p,
Matcher: upstreamMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
},
},
SearchMatches: map[string]interface{}{
"namespace": vuln.Namespace,
"versionConstraint": vuln.Constraint.String(),
},
})
if !isPackageVulnerable {
continue
}
matches = append(matches, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: vuln,
Package: p,
MatchDetails: []match.Details{
{
Matcher: upstreamMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": d.Type.String(),
"version": d.RawVersion,
},
"namespace": vuln.Namespace,
},
MatchedOn: map[string]interface{}{
"versionConstraint": vuln.Constraint.String(),
},
Confidence: 1.0, // TODO: this is hard coded for now
},
},
})
}
return matches, err

View file

@ -14,19 +14,19 @@ import (
)
type mockDistroProvider struct {
data map[string]map[string][]*vulnerability.Vulnerability
data map[string]map[string][]vulnerability.Vulnerability
}
func newMockProviderByDistro() *mockDistroProvider {
pr := mockDistroProvider{
data: make(map[string]map[string][]*vulnerability.Vulnerability),
data: make(map[string]map[string][]vulnerability.Vulnerability),
}
pr.stub()
return &pr
}
func (pr *mockDistroProvider) stub() {
pr.data["debian:8"] = map[string][]*vulnerability.Vulnerability{
pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{
// direct...
"neutron": {
{
@ -55,7 +55,7 @@ func (pr *mockDistroProvider) stub() {
}
}
func (pr *mockDistroProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]*vulnerability.Vulnerability, error) {
func (pr *mockDistroProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil
}
@ -80,19 +80,23 @@ func TestFindMatchesByPackageDistro(t *testing.T) {
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2014-fake-1",
},
Confidence: 1,
Package: p,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
Package: p,
MatchDetails: []match.Details{
{
Confidence: 1,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
},
"namespace": "debian:8",
},
MatchedOn: map[string]interface{}{
"versionConstraint": "< 2014.1.5-6 (deb)",
},
Matcher: match.PythonMatcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "debian:8",
"versionConstraint": "< 2014.1.5-6 (deb)",
},
Matcher: match.PythonMatcher,
},
}

View file

@ -29,22 +29,28 @@ func FindMatchesByPackageLanguage(store vulnerability.ProviderByLanguage, l syft
return nil, fmt.Errorf("language matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err)
}
if isPackageVulnerable {
matches = append(matches, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0, // TODO: this is hard coded for now
Vulnerability: *vuln,
Package: p,
Matcher: upstreamMatcher,
SearchedBy: map[string]interface{}{
"language": l.String(),
},
SearchMatches: map[string]interface{}{
"namespace": vuln.Namespace,
"versionConstraint": vuln.Constraint.String(),
},
})
if !isPackageVulnerable {
continue
}
matches = append(matches, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: vuln,
Package: p,
MatchDetails: []match.Details{
{
Confidence: 1.0, // TODO: this is hard coded for now
Matcher: upstreamMatcher,
SearchedBy: map[string]interface{}{
"language": l.String(),
"namespace": vuln.Namespace,
},
MatchedOn: map[string]interface{}{
"versionConstraint": vuln.Constraint.String(),
},
},
},
})
}
return matches, err

View file

@ -14,19 +14,19 @@ import (
)
type mockLanguageProvider struct {
data map[string]map[string][]*vulnerability.Vulnerability
data map[string]map[string][]vulnerability.Vulnerability
}
func newMockProviderByLanguage() *mockLanguageProvider {
pr := mockLanguageProvider{
data: make(map[string]map[string][]*vulnerability.Vulnerability),
data: make(map[string]map[string][]vulnerability.Vulnerability),
}
pr.stub()
return &pr
}
func (pr *mockLanguageProvider) stub() {
pr.data["github:gem"] = map[string][]*vulnerability.Vulnerability{
pr.data["github:gem"] = map[string][]vulnerability.Vulnerability{
// direct...
"activerecord": {
{
@ -43,7 +43,7 @@ func (pr *mockLanguageProvider) stub() {
}
}
func (pr *mockLanguageProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]*vulnerability.Vulnerability, error) {
func (pr *mockLanguageProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
if l != syftPkg.Ruby {
panic(fmt.Errorf("test mock only supports ruby"))
}
@ -64,16 +64,20 @@ func TestFindMatchesByPackageLanguage(t *testing.T) {
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2017-fake-1",
},
Confidence: 1,
Package: p,
SearchedBy: map[string]interface{}{
"language": "ruby",
Package: p,
MatchDetails: []match.Details{
{
Confidence: 1,
SearchedBy: map[string]interface{}{
"language": "ruby",
"namespace": "github:ruby",
},
MatchedOn: map[string]interface{}{
"versionConstraint": "< 3.7.6 (semver)",
},
Matcher: match.RubyGemMatcher,
},
},
SearchMatches: map[string]interface{}{
"namespace": "github:ruby",
"versionConstraint": "< 3.7.6 (semver)",
},
Matcher: match.RubyGemMatcher,
},
}

View file

@ -6,9 +6,11 @@ import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func assertMatchesUsingIDsForVulnerabilities(t testing.TB, expected, actual []match.Match) {
assert.Len(t, actual, len(expected))
for idx, a := range actual {
// only compare the vulnerability ID, nothing else
a.Vulnerability = vulnerability.Vulnerability{ID: a.Vulnerability.ID}

View file

@ -74,7 +74,6 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.ProviderByDistro,
for idx := range matches {
matches[idx].Type = match.ExactIndirectMatch
matches[idx].Package = p
matches[idx].Matcher = m.Type()
}
return matches, nil

View file

@ -10,19 +10,19 @@ import (
)
type mockProvider struct {
data map[string]map[string][]*vulnerability.Vulnerability
data map[string]map[string][]vulnerability.Vulnerability
}
func newMockProvider() *mockProvider {
pr := mockProvider{
data: make(map[string]map[string][]*vulnerability.Vulnerability),
data: make(map[string]map[string][]vulnerability.Vulnerability),
}
pr.stub()
return &pr
}
func (pr *mockProvider) stub() {
pr.data["debian:8"] = map[string][]*vulnerability.Vulnerability{
pr.data["debian:8"] = map[string][]vulnerability.Vulnerability{
// direct...
"neutron": {
{
@ -50,6 +50,6 @@ func (pr *mockProvider) stub() {
}
}
func (pr *mockProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]*vulnerability.Vulnerability, error) {
func (pr *mockProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
return pr.data[strings.ToLower(d.Type.String())+":"+d.FullVersion()][p.Name], nil
}

View file

@ -3,6 +3,8 @@ package dpkg
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/internal"
@ -29,27 +31,17 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
store := newMockProvider()
actual, err := matcher.matchBySourceIndirection(store, &d, p)
if len(actual) != 2 {
t.Fatalf("unexpected indirect matches count: %d", len(actual))
}
assert.Len(t, actual, 2, "unexpected indirect matches count")
foundCVEs := internal.NewStringSet()
for _, a := range actual {
foundCVEs.Add(a.Vulnerability.ID)
if a.Type != match.ExactIndirectMatch {
t.Error("indirect match not indicated")
assert.Equal(t, match.ExactIndirectMatch, a.Type, "indirect match not indicated")
assert.Equal(t, p.Name, a.Package.Name, "failed to capture original package name")
for _, detail := range a.MatchDetails {
assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type")
}
if a.Package.Name != p.Name {
t.Errorf("failed to capture correct original package: %s", a.Package.Name)
}
if a.Matcher != matcher.Type() {
t.Errorf("failed to capture matcher type: %s", a.Matcher)
}
}
for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} {
@ -60,5 +52,4 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
if t.Failed() {
t.Logf("discovered CVES: %+v", foundCVEs)
}
}

View file

@ -95,7 +95,6 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.ProviderByDistro,
for idx := range matches {
matches[idx].Type = match.ExactIndirectMatch
matches[idx].Package = p
matches[idx].Matcher = m.Type()
}
return matches, nil

View file

@ -10,19 +10,19 @@ import (
)
type mockProvider struct {
data map[string]map[string][]*vulnerability.Vulnerability
data map[string]map[string][]vulnerability.Vulnerability
}
func newMockProvider() *mockProvider {
pr := mockProvider{
data: make(map[string]map[string][]*vulnerability.Vulnerability),
data: make(map[string]map[string][]vulnerability.Vulnerability),
}
pr.stub()
return &pr
}
func (pr *mockProvider) stub() {
pr.data["rhel:8"] = map[string][]*vulnerability.Vulnerability{
pr.data["rhel:8"] = map[string][]vulnerability.Vulnerability{
// direct...
"neutron-libs": {
{
@ -50,7 +50,7 @@ func (pr *mockProvider) stub() {
}
}
func (pr *mockProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]*vulnerability.Vulnerability, error) {
func (pr *mockProvider) GetByDistro(d distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
var ty = strings.ToLower(d.Type.String())
if d.Type == distro.CentOS || d.Type == distro.RedHat {
ty = "rhel"

View file

@ -3,6 +3,8 @@ package rpmdb
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/internal"
@ -29,25 +31,17 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
store := newMockProvider()
actual, err := matcher.matchBySourceIndirection(store, &d, p)
if len(actual) != 2 {
t.Fatalf("unexpected indirect matches count: %d", len(actual))
}
assert.Len(t, actual, 2, "unexpected indirect matches count")
foundCVEs := internal.NewStringSet()
for _, a := range actual {
foundCVEs.Add(a.Vulnerability.ID)
if a.Type != match.ExactIndirectMatch {
t.Error("indirect match not indicated")
}
if a.Package.Name != p.Name {
t.Errorf("failed to capture correct original package: %s", a.Package.Name)
}
if a.Matcher != matcher.Type() {
t.Errorf("failed to capture matcher type: %s", a.Matcher)
assert.Equal(t, match.ExactIndirectMatch, a.Type, "indirect match not indicated")
assert.Equal(t, p.Name, a.Package.Name, "failed to capture original package name")
for _, detail := range a.MatchDetails {
assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type")
}
}
@ -82,8 +76,5 @@ func TestMatcherDpkg_matchBySourceIndirection_ignoreSource(t *testing.T) {
store := newMockProvider()
actual, err := matcher.matchBySourceIndirection(store, &d, p)
if len(actual) != 0 {
t.Fatalf("unexpected indirect matches count: %d", len(actual))
}
assert.Len(t, actual, 0, "unexpected indirect matches count")
}

View file

@ -13,7 +13,7 @@ type ID string
// Package represents an application or library that has been bundled into a distributable format.
type Package struct {
id ID
ID ID
Name string // the package name
Version string // the version of the package
Locations []source.Location // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
@ -66,7 +66,7 @@ func New(p *pkg.Package) Package {
}
return Package{
id: ID(p.ID),
ID: ID(p.ID),
Name: p.Name,
Version: p.Version,
Locations: p.Locations,
@ -87,11 +87,6 @@ func FromCatalog(catalog *pkg.Catalog) []Package {
return result
}
// ID returns the package ID, which is unique relative to a package catalog.
func (p Package) ID() ID {
return p.id
}
// Stringer to represent a package.
func (p Package) String() string {
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
@ -99,7 +94,7 @@ func (p Package) String() string {
func ByID(id ID, pkgs []Package) *Package {
for _, p := range pkgs {
if p.ID() == id {
if p.ID == id {
return &p
}
}

View file

@ -97,8 +97,8 @@ func TestNew_MetadataExtraction(t *testing.T) {
PomProperties: &syftPkg.PomProperties{
Path: "pom-path-info",
Name: "pom-name-info",
GroupID: "pom-group-id-info",
ArtifactID: "pom-artifact-id-info",
GroupID: "pom-group-ID-info",
ArtifactID: "pom-artifact-ID-info",
Version: "pom-version-info",
Extra: map[string]string{
"extra-key": "extra-value",
@ -108,8 +108,8 @@ func TestNew_MetadataExtraction(t *testing.T) {
},
metadata: JavaMetadata{
VirtualPath: "virtual-path-info",
PomArtifactID: "pom-artifact-id-info",
PomGroupID: "pom-group-id-info",
PomArtifactID: "pom-artifact-ID-info",
PomGroupID: "pom-group-ID-info",
ManifestName: "main-section-name-info",
},
},

View file

@ -83,7 +83,7 @@ type partialSyftPackage struct {
// packageBasicMetadata contains non-ambiguous values (type-wise) from pkg.Package.
type packageBasicMetadata struct {
ID string `json:"id"`
ID string `json:"ID"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
@ -202,7 +202,7 @@ func parseSyftJSON(reader io.Reader) ([]Package, Context, error) {
}
packages[i] = Package{
id: ID(a.ID),
ID: ID(a.ID),
Name: a.Name,
Version: a.Version,
Locations: a.Locations,

View file

@ -63,7 +63,7 @@ func NewDocument(packages []pkg.Package, matches match.Matches, srcMetadata *sou
// mutate the Component
pkgMatches := matches.GetByPkgID(p.ID())
pkgMatches := matches.GetByPkgID(p.ID)
if len(pkgMatches) > 0 {
var vulnerabilities []Vulnerability

View file

@ -10,7 +10,7 @@ import (
"github.com/anchore/syft/syft/source"
)
// Presenter writes a CycloneDX report from the given Catalog and Scope contents
// Presenter writes a CycloneDX report from the given Matches and Scope contents
type Presenter struct {
results match.Matches
packages []pkg.Package

View file

@ -50,7 +50,11 @@ func createResults() (match.Matches, []pkg.Package) {
Namespace: "source-1",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
},
},
}
var match2 = match.Match{
@ -60,9 +64,13 @@ func createResults() (match.Matches, []pkg.Package) {
Namespace: "source-2",
},
Package: pkg2,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"some": "key",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"some": "key",
},
},
},
}

View file

@ -76,16 +76,20 @@ func TestJsonImgsPresenter(t *testing.T) {
},
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
},
},
MatchedOn: map[string]interface{}{
"constraint": ">= 20",
},
},
},
SearchMatches: map[string]interface{}{
"constraint": ">= 20",
},
}
var match2 = match.Match{
@ -95,12 +99,16 @@ func TestJsonImgsPresenter(t *testing.T) {
Namespace: "source-2",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
SearchMatches: map[string]interface{}{
"constraint": "somecpe",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
MatchedOn: map[string]interface{}{
"constraint": "somecpe",
},
},
},
}
@ -116,12 +124,16 @@ func TestJsonImgsPresenter(t *testing.T) {
},
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"language": "java",
},
SearchMatches: map[string]interface{}{
"constraint": "< 2.0.0",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"language": "java",
},
MatchedOn: map[string]interface{}{
"constraint": "< 2.0.0",
},
},
},
}
@ -210,16 +222,20 @@ func TestJsonDirsPresenter(t *testing.T) {
},
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
},
},
MatchedOn: map[string]interface{}{
"constraint": ">= 20",
},
},
},
SearchMatches: map[string]interface{}{
"constraint": ">= 20",
},
}
var match2 = match.Match{
@ -229,12 +245,16 @@ func TestJsonDirsPresenter(t *testing.T) {
Namespace: "source-2",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
SearchMatches: map[string]interface{}{
"constraint": "somecpe",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
MatchedOn: map[string]interface{}{
"constraint": "somecpe",
},
},
},
}
@ -250,12 +270,16 @@ func TestJsonDirsPresenter(t *testing.T) {
},
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"language": "java",
},
SearchMatches: map[string]interface{}{
"constraint": "< 2.0.0",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"language": "java",
},
MatchedOn: map[string]interface{}{
"constraint": "< 2.0.0",
},
},
},
}

View file

@ -26,18 +26,20 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"distro": {
"type": "ubuntu",
"version": "20.04"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"distro": {
"type": "ubuntu",
"version": "20.04"
}
},
"matchedOn": {
"constraint": ">= 20"
}
},
"matchedOn": {
"constraint": ">= 20"
}
},
],
"artifact": {
"name": "package-1",
"version": "1.0.1",
@ -85,15 +87,17 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"cpe": "somecpe"
},
"matchedOn": {
"constraint": "somecpe"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"cpe": "somecpe"
},
"matchedOn": {
"constraint": "somecpe"
}
}
},
],
"artifact": {
"name": "package-1",
"version": "1.0.1",
@ -129,15 +133,17 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"language": "java"
},
"matchedOn": {
"constraint": "< 2.0.0"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"language": "java"
},
"matchedOn": {
"constraint": "< 2.0.0"
}
}
},
],
"artifact": {
"name": "package-1",
"version": "1.0.1",

View file

@ -26,18 +26,20 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"distro": {
"type": "ubuntu",
"version": "20.04"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"distro": {
"type": "ubuntu",
"version": "20.04"
}
},
"matchedOn": {
"constraint": ">= 20"
}
},
"matchedOn": {
"constraint": ">= 20"
}
},
],
"artifact": {
"name": "package-1",
"version": "1.1.1",
@ -88,15 +90,17 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"cpe": "somecpe"
},
"matchedOn": {
"constraint": "somecpe"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"cpe": "somecpe"
},
"matchedOn": {
"constraint": "somecpe"
}
}
},
],
"artifact": {
"name": "package-1",
"version": "1.1.1",
@ -135,15 +139,17 @@
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": {
"matcher": "dpkg-matcher",
"searchedBy": {
"language": "java"
},
"matchedOn": {
"constraint": "< 2.0.0"
"matchDetails": [
{
"matcher": "dpkg-matcher",
"searchedBy": {
"language": "java"
},
"matchedOn": {
"constraint": "< 2.0.0"
}
}
},
],
"artifact": {
"name": "package-1",
"version": "1.1.1",

View file

@ -24,7 +24,7 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc
// we must preallocate the findings to ensure the JSON document does not show "null" when no matches are found
var findings = make([]Match, 0)
for _, m := range matches.Sorted() {
p := pkg.ByID(m.Package.ID(), packages)
p := pkg.ByID(m.Package.ID, packages)
if p == nil {
return Document{}, fmt.Errorf("unable to find package in collection: %+v", p)
}

View file

@ -12,15 +12,15 @@ import (
type Match struct {
Vulnerability Vulnerability `json:"vulnerability"`
RelatedVulnerabilities []VulnerabilityMetadata `json:"relatedVulnerabilities"`
MatchDetails MatchDetails `json:"matchDetails"`
MatchDetails []MatchDetails `json:"matchDetails"`
Artifact Package `json:"artifact"`
}
// MatchDetails contains all data that indicates how the result match was found
type MatchDetails struct {
Matcher string `json:"matcher"`
SearchedBy map[string]interface{} `json:"searchedBy"`
MatchedOn map[string]interface{} `json:"matchedOn"`
Matcher string `json:"matcher"`
SearchedBy interface{} `json:"searchedBy"`
MatchedOn interface{} `json:"matchedOn"`
}
func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) {
@ -40,14 +40,19 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad
return nil, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
}
details := make([]MatchDetails, len(m.MatchDetails))
for idx, d := range m.MatchDetails {
details[idx] = MatchDetails{
Matcher: d.Matcher.String(),
SearchedBy: d.SearchedBy,
MatchedOn: d.MatchedOn,
}
}
return &Match{
Vulnerability: NewVulnerability(m.Vulnerability, metadata),
Artifact: newPackage(p),
RelatedVulnerabilities: relatedVulnerabilities,
MatchDetails: MatchDetails{
Matcher: m.Matcher.String(),
SearchedBy: m.SearchedBy,
MatchedOn: m.SearchMatches,
},
MatchDetails: details,
}, nil
}

View file

@ -39,16 +39,20 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches {
},
},
Package: p,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
},
},
MatchedOn: map[string]interface{}{
"constraint": ">= 20",
},
},
},
SearchMatches: map[string]interface{}{
"constraint": ">= 20",
},
},
{
Type: match.ExactIndirectMatch,
@ -57,12 +61,16 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches {
Namespace: "source-2",
},
Package: p,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
SearchMatches: map[string]interface{}{
"constraint": "somecpe",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"cpe": "somecpe",
},
MatchedOn: map[string]interface{}{
"constraint": "somecpe",
},
},
},
},
}

View file

@ -41,7 +41,11 @@ func TestTablePresenter(t *testing.T) {
Namespace: "source-1",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
},
},
}
var match2 = match.Match{
@ -56,9 +60,13 @@ func TestTablePresenter(t *testing.T) {
},
},
Package: pkg2,
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"some": "key",
MatchDetails: []match.Details{
{
Matcher: match.DpkgMatcher,
SearchedBy: map[string]interface{}{
"some": "key",
},
},
},
}

View file

@ -2,17 +2,18 @@ package template
import (
"bytes"
"flag"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/grype/grype/presenter/models"
"github.com/anchore/go-testutils"
"github.com/anchore/grype/grype/presenter/models"
"github.com/stretchr/testify/assert"
)
var update = flag.Bool("update", false, "update the *.golden files for template presenters")
func TestPresenter_Present(t *testing.T) {
matches, packages, context, metadataProvider, appConfig, dbStatus := models.GenerateAnalysis(t)
@ -30,6 +31,9 @@ func TestPresenter_Present(t *testing.T) {
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
expected := testutils.GetGoldenFileContents(t)
assert.Equal(t, string(expected), string(actual))

View file

@ -1,12 +1,10 @@
Identified distro as centos version 8.0.
Vulnerability: CVE-1999-0001
Severity: Low
Matched by: dpkg-matcher
Package: package-1 version 1.1.1 (deb)
Matched by: dpkg-matcher
Vulnerability: CVE-1999-0002
Severity: Critical
Matched by: dpkg-matcher
Package: package-1 version 1.1.1 (deb)
Matched by: dpkg-matcher

View file

@ -1,9 +1,10 @@
Identified distro as {{.Distro.Name}} version {{.Distro.Version}}.
{{- range .Matches}}
Vulnerability: {{.Vulnerability.ID}}
Severity: {{.Vulnerability.Severity}}
Matched by: {{.MatchDetails.Matcher}}
Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}})
{{- range .MatchDetails}}
Matched by: {{.Matcher}}
{{- end}}
{{- end}}

View file

@ -13,15 +13,15 @@ type Provider interface {
}
type ProviderByDistro interface {
GetByDistro(distro.Distro, pkg.Package) ([]*Vulnerability, error)
GetByDistro(distro.Distro, pkg.Package) ([]Vulnerability, error)
}
type ProviderByLanguage interface {
GetByLanguage(syftPkg.Language, pkg.Package) ([]*Vulnerability, error)
GetByLanguage(syftPkg.Language, pkg.Package) ([]Vulnerability, error)
}
type ProviderByCPE interface {
GetByCPE(syftPkg.CPE) ([]*Vulnerability, error)
GetByCPE(syftPkg.CPE) ([]Vulnerability, error)
}
type MetadataProvider interface {

View file

@ -21,8 +21,8 @@ func NewProviderFromStore(store db.VulnerabilityStoreReader) *StoreAdapter {
}
}
func (pr *StoreAdapter) GetByDistro(d distro.Distro, p pkg.Package) ([]*Vulnerability, error) {
vulns := make([]*Vulnerability, 0)
func (pr *StoreAdapter) GetByDistro(d distro.Distro, p pkg.Package) ([]Vulnerability, error) {
vulns := make([]Vulnerability, 0)
namespace := distroNamespace(d)
allPkgVulns, err := pr.store.GetVulnerability(namespace, p.Name)
@ -36,14 +36,14 @@ func (pr *StoreAdapter) GetByDistro(d distro.Distro, p pkg.Package) ([]*Vulnerab
return nil, fmt.Errorf("provider failed to parse distro='%s': %w", d, err)
}
vulns = append(vulns, vulnObj)
vulns = append(vulns, *vulnObj)
}
return vulns, nil
}
func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]*Vulnerability, error) {
vulns := make([]*Vulnerability, 0)
func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]Vulnerability, error) {
vulns := make([]Vulnerability, 0)
namespaces := languageNamespaces(l)
if namespaces == nil {
@ -63,7 +63,7 @@ func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]*Vul
return nil, fmt.Errorf("provider failed to parse language='%s': %w", l, err)
}
vulns = append(vulns, vulnObj)
vulns = append(vulns, *vulnObj)
}
}
}
@ -71,8 +71,8 @@ func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]*Vul
return vulns, nil
}
func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]*Vulnerability, error) {
vulns := make([]*Vulnerability, 0)
func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]Vulnerability, error) {
vulns := make([]Vulnerability, 0)
namespaces := cpeNamespaces()
if namespaces == nil {
@ -106,7 +106,7 @@ func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]*Vulnerability, erro
vulnObj.CPEs = candidateMatchCpes
vulns = append(vulns, vulnObj)
vulns = append(vulns, *vulnObj)
}
}
}

View file

@ -50,7 +50,7 @@ func TestGetByDistro(t *testing.T) {
assert.Len(t, actual, len(expected))
for idx, vuln := range actual {
for _, d := range deep.Equal(&expected[idx], vuln) {
for _, d := range deep.Equal(expected[idx], vuln) {
t.Errorf("diff: %+v", d)
}
}
@ -135,7 +135,7 @@ func TestGetByCPE(t *testing.T) {
assert.Len(t, actual, len(test.expected))
for idx, vuln := range actual {
for _, d := range deep.Equal(&test.expected[idx], vuln) {
for _, d := range deep.Equal(test.expected[idx], vuln) {
t.Errorf("diff: %+v", d)
}
}

View file

@ -30,17 +30,21 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
theResult.Add(thePkg, match.Match{
// note: we are matching on the secdb record, not NVD primarily
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*",
},
MatchedOn: map[string]interface{}{
"cpes": []string{"cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
"constraint": "< 0.9.10 (unknown)",
},
Matcher: match.ApkMatcher,
},
},
SearchMatches: map[string]interface{}{
"cpes": []string{"cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
"constraint": "< 0.9.10 (unknown)",
},
Matcher: match.ApkMatcher,
})
}
@ -58,16 +62,20 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"language": "javascript",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "javascript",
},
MatchedOn: map[string]interface{}{
"constraint": "< 3.2.1 (unknown)",
},
Matcher: match.JavascriptMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": "< 3.2.1 (unknown)",
},
Matcher: match.JavascriptMatcher,
})
}
@ -88,16 +96,20 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"language": "python",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "python",
},
MatchedOn: map[string]interface{}{
"constraint": "< 2.6.2 (python)",
},
Matcher: match.PythonMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": "< 2.6.2 (python)",
},
Matcher: match.PythonMatcher,
})
}
@ -115,16 +127,20 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"language": "ruby",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "ruby",
},
MatchedOn: map[string]interface{}{
"constraint": "> 4.0.0, <= 4.1.1 (semver)",
},
Matcher: match.RubyGemMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": "> 4.0.0, <= 4.1.1 (semver)",
},
Matcher: match.RubyGemMatcher,
})
}
@ -151,16 +167,20 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"language": "java",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "java",
},
MatchedOn: map[string]interface{}{
"constraint": ">= 0.0.1, < 1.2.0 (unknown)",
},
Matcher: match.JavaMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": ">= 0.0.1, < 1.2.0 (unknown)",
},
Matcher: match.JavaMatcher,
})
}
@ -179,19 +199,23 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
}
theResult.Add(thePkg, match.Match{
Type: match.ExactIndirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
},
},
MatchedOn: map[string]interface{}{
"constraint": "<= 1.8.2 (deb)",
},
Matcher: match.DpkgMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": "<= 1.8.2 (deb)",
},
Matcher: match.DpkgMatcher,
})
}
@ -209,19 +233,23 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Confidence: 1.0,
Vulnerability: *vulnObj,
Package: thePkg,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "centos",
"version": "8",
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "centos",
"version": "8",
},
},
MatchedOn: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
},
Matcher: match.RpmDBMatcher,
},
},
SearchMatches: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
},
Matcher: match.RpmDBMatcher,
})
}
@ -310,7 +338,9 @@ func TestPkgCoverageImage(t *testing.T) {
actualCount := 0
for aMatch := range actualResults.Enumerate() {
actualCount++
observedMatchers.Add(aMatch.Matcher.String())
for _, details := range aMatch.MatchDetails {
observedMatchers.Add(details.Matcher.String())
}
value, ok := expectedMatchSet[aMatch.Package.Name]
if !ok {
t.Errorf("Package: %s was expected but not found", aMatch.Package.Name)
@ -327,12 +357,10 @@ func TestPkgCoverageImage(t *testing.T) {
if expectedCount != actualCount {
t.Errorf("expected %d matches but got %d matches", expectedCount, actualCount)
}
})
}
// ensure that integration test cases stay in sync with the implemented matchers
observedMatchers.Remove(match.UnknownMatcherType.String())
definedMatchers.Remove(match.UnknownMatcherType.String())
definedMatchers.Remove(match.MsrcMatcher.String())