Optionally orient results by CVE (#1020)

Co-authored-by: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Alex Goodman 2022-12-08 15:22:40 -05:00 committed by GitHub
parent ef82b33465
commit a869480f89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1781 additions and 613 deletions

View file

@ -1,6 +1,7 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
@ -21,6 +22,7 @@ import (
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/python"
"github.com/anchore/grype/grype/matcher/ruby"
@ -167,6 +169,11 @@ func setRootFlags(flags *pflag.FlagSet) {
"ignore matches for vulnerabilities that are fixed",
)
flags.BoolP(
"by-cve", "", false,
"orient results by CVE instead of the original vulnerability ID when possible",
)
flags.BoolP(
"show-suppressed", "", false,
"show suppressed/ignored vulnerabilities in the output (only supported with table output format)",
@ -229,6 +236,10 @@ func bindRootConfigOptions(flags *pflag.FlagSet) error {
return err
}
if err := viper.BindPFlag("by-cve", flags.Lookup("by-cve")); err != nil {
return err
}
if err := viper.BindPFlag("show-suppressed", flags.Lookup("show-suppressed")); err != nil {
return err
}
@ -298,28 +309,13 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
return
}
if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
checkForAppUpdate()
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("No new %s update available", internal.ApplicationName)
}
}
var store *store.Store
var str *store.Store
var status *db.Status
var dbCloser *db.Closer
var packages []pkg.Package
var context pkg.Context
var pkgContext pkg.Context
var wg = &sync.WaitGroup{}
var loadedDB, gatheredPackages bool
@ -328,7 +324,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
go func() {
defer wg.Done()
log.Debug("loading DB")
store, status, dbCloser, err = grype.LoadVulnerabilityDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate)
str, status, dbCloser, err = grype.LoadVulnerabilityDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate)
if err = validateDBLoad(err, status); err != nil {
errs <- err
return
@ -339,7 +335,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
go func() {
defer wg.Done()
log.Debugf("gathering packages")
packages, context, err = pkg.Provide(userInput, getProviderConfig())
packages, pkgContext, err = pkg.Provide(userInput, getProviderConfig())
if err != nil {
errs <- fmt.Errorf("failed to catalog: %w", err)
return
@ -364,35 +360,27 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
appConfig.Ignore = append(appConfig.Ignore, ignoreFixedMatches...)
}
applyDistroHint(packages, &context, appConfig)
applyDistroHint(packages, &pkgContext, appConfig)
matchers := matcher.NewDefaultMatchers(matcher.Config{
Java: appConfig.ExternalSources.ToJavaMatcherConfig(appConfig.Match.Java),
Ruby: ruby.MatcherConfig(appConfig.Match.Ruby),
Python: python.MatcherConfig(appConfig.Match.Python),
Dotnet: dotnet.MatcherConfig(appConfig.Match.Dotnet),
Javascript: javascript.MatcherConfig(appConfig.Match.Javascript),
Golang: golang.MatcherConfig(appConfig.Match.Golang),
Stock: stock.MatcherConfig(appConfig.Match.Stock),
})
allMatches := grype.FindVulnerabilitiesForPackage(*store, context.Distro, matchers, packages)
remainingMatches, ignoredMatches := match.ApplyIgnoreRules(allMatches, appConfig.Ignore)
if count := len(ignoredMatches); count > 0 {
log.Infof("ignoring %d matches due to user-provided ignore rules", count)
vulnMatcher := grype.VulnerabilityMatcher{
Store: *str,
IgnoreRules: appConfig.Ignore,
NormalizeByCVE: appConfig.ByCVE,
FailSeverity: failOnSeverity,
Matchers: getMatchers(),
}
// determine if there are any severities >= to the max allowable severity (which is optional).
// note: until the shared file lock in sqlittle is fixed the sqlite DB cannot be access concurrently,
// implying that the fail-on-severity check must be done before sending the presenter object.
if hitSeverityThreshold(failOnSeverity, remainingMatches, store) {
errs <- grypeerr.ErrAboveSeverityThreshold
remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
if err != nil {
errs <- err
if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
return
}
}
bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter.GetPresenter(presenterConfig, remainingMatches, ignoredMatches, packages, context, store, appConfig, status),
Value: presenter.GetPresenter(presenterConfig, *remainingMatches, ignoredMatches, packages, pkgContext, str, appConfig, status),
})
}()
return errs
@ -434,15 +422,57 @@ func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, appConfig *config
}
}
func checkForAppUpdate() {
if !appConfig.CheckForAppUpdate {
return
}
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
func getMatchers() []matcher.Matcher {
return matcher.NewDefaultMatchers(
matcher.Config{
Java: java.MatcherConfig{
ExternalSearchConfig: appConfig.ExternalSources.ToJavaMatcherConfig(),
UseCPEs: appConfig.Match.Java.UseCPEs,
},
Ruby: ruby.MatcherConfig(appConfig.Match.Ruby),
Python: python.MatcherConfig(appConfig.Match.Python),
Dotnet: dotnet.MatcherConfig(appConfig.Match.Dotnet),
Javascript: javascript.MatcherConfig(appConfig.Match.Javascript),
Golang: golang.MatcherConfig(appConfig.Match.Golang),
Stock: stock.MatcherConfig(appConfig.Match.Stock),
},
)
}
func getProviderConfig() pkg.ProviderConfig {
return pkg.ProviderConfig{
RegistryOptions: appConfig.Registry.ToOptions(),
Exclusions: appConfig.Exclusions,
CatalogingOptions: appConfig.Search.ToConfig(),
GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
Platform: appConfig.Platform,
AttestationPublicKey: appConfig.Attestation.PublicKey,
AttestationIgnoreVerification: appConfig.Attestation.SkipVerification,
SyftProviderConfig: pkg.SyftProviderConfig{
RegistryOptions: appConfig.Registry.ToOptions(),
Exclusions: appConfig.Exclusions,
CatalogingOptions: appConfig.Search.ToConfig(),
Platform: appConfig.Platform,
AttestationPublicKey: appConfig.Attestation.PublicKey,
AttestationIgnoreVerification: appConfig.Attestation.SkipVerification,
},
SynthesisConfig: pkg.SynthesisConfig{
GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
},
}
}
@ -476,25 +506,3 @@ func validateRootArgs(cmd *cobra.Command, args []string) error {
return cobra.MaximumNArgs(1)(cmd, args)
}
// hitSeverityThreshold indicates if there are any severities >= to the max allowable severity (which is optional)
func hitSeverityThreshold(thresholdSeverity *vulnerability.Severity, matches match.Matches, metadataProvider vulnerability.MetadataProvider) bool {
if thresholdSeverity != nil {
var maxDiscoveredSeverity vulnerability.Severity
for m := range matches.Enumerate() {
metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
if err != nil {
continue
}
severity := vulnerability.ParseSeverity(metadata.Severity)
if severity > maxDiscoveredSeverity {
maxDiscoveredSeverity = severity
}
}
if maxDiscoveredSeverity >= *thresholdSeverity {
return true
}
}
return false
}

View file

@ -3,122 +3,12 @@ package cmd
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/config"
syftPkg "github.com/anchore/syft/syft/pkg"
)
type mockMetadataStore struct {
data map[string]map[string]*grypeDB.VulnerabilityMetadata
}
func newMockStore() *mockMetadataStore {
d := mockMetadataStore{
data: make(map[string]map[string]*grypeDB.VulnerabilityMetadata),
}
d.stub()
return &d
}
func (d *mockMetadataStore) stub() {
d.data["CVE-2014-fake-1"] = map[string]*grypeDB.VulnerabilityMetadata{
"source-1": {
Severity: "medium",
},
}
}
func (d *mockMetadataStore) GetVulnerabilityMetadata(id, recordSource string) (*grypeDB.VulnerabilityMetadata, error) {
return d.data[id][recordSource], nil
}
func (d *mockMetadataStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
return nil, nil
}
func TestAboveAllowableSeverity(t *testing.T) {
thePkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "the-package",
Version: "v0.1",
Type: syftPkg.RpmPkg,
}
matches := match.NewMatches()
matches.Add(match.Match{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2014-fake-1",
Namespace: "source-1",
},
Package: thePkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
},
},
})
tests := []struct {
name string
failOnSeverity string
matches match.Matches
expectedResult bool
}{
{
name: "no-severity-set",
failOnSeverity: "",
matches: matches,
expectedResult: false,
},
{
name: "below-threshold",
failOnSeverity: "high",
matches: matches,
expectedResult: false,
},
{
name: "at-threshold",
failOnSeverity: "medium",
matches: matches,
expectedResult: true,
},
{
name: "above-threshold",
failOnSeverity: "low",
matches: matches,
expectedResult: true,
},
}
metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var failOnSeverity *vulnerability.Severity
if test.failOnSeverity != "" {
sev := vulnerability.ParseSeverity(test.failOnSeverity)
if sev == vulnerability.UnknownSeverity {
t.Fatalf("could not parse severity")
}
failOnSeverity = &sev
}
actual := hitSeverityThreshold(failOnSeverity, test.matches, metadataProvider)
if test.expectedResult != actual {
t.Errorf("expected: %v got : %v", test.expectedResult, actual)
}
})
}
}
func Test_applyDistroHint(t *testing.T) {
ctx := pkg.Context{}
cfg := config.Application{}

2
go.mod
View file

@ -269,7 +269,7 @@ require (
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 // indirect
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect

4
go.sum
View file

@ -2323,8 +2323,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View file

@ -95,8 +95,26 @@ func (s *store) GetVulnerabilityNamespaces() ([]string, error) {
return names, result.Error
}
// GetVulnerability retrieves vulnerabilities by namespace and package
func (s *store) GetVulnerability(namespace, packageName string) ([]v5.Vulnerability, error) {
// GetVulnerability retrieves vulnerabilities by namespace and id
func (s *store) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) {
var models []model.VulnerabilityModel
result := s.db.Where("namespace = ? AND id = ?", namespace, id).Find(&models)
var vulnerabilities = make([]v5.Vulnerability, len(models))
for idx, m := range models {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities[idx] = vulnerability
}
return vulnerabilities, result.Error
}
// SearchForVulnerabilities retrieves vulnerabilities by namespace and package
func (s *store) SearchForVulnerabilities(namespace, packageName string) ([]v5.Vulnerability, error) {
var models []model.VulnerabilityModel
result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)

View file

@ -55,7 +55,7 @@ func TestStore_GetID_SetID(t *testing.T) {
}
func assertVulnerabilityReader(t *testing.T, reader v5.VulnerabilityStoreReader, namespace, name string, expected []v5.Vulnerability) {
if actual, err := reader.GetVulnerability(namespace, name); err != nil {
if actual, err := reader.SearchForVulnerabilities(namespace, name); err != nil {
t.Fatalf("failed to get Vulnerability: %+v", err)
} else {
if len(actual) != len(expected) {

View file

@ -10,8 +10,10 @@ type VulnerabilityStore interface {
type VulnerabilityStoreReader interface {
// GetVulnerabilityNamespaces retrieves unique list of vulnerability namespaces
GetVulnerabilityNamespaces() ([]string, error)
// GetVulnerability retrieves vulnerabilities by namespace and package
GetVulnerability(namespace, packageName string) ([]Vulnerability, error)
// GetVulnerability retrieves vulnerabilities by namespace and id
GetVulnerability(namespace, id string) ([]Vulnerability, error)
// SearchForVulnerabilities retrieves vulnerabilities by namespace and package
SearchForVulnerabilities(namespace, packageName string) ([]Vulnerability, error)
GetAllVulnerabilities() (*[]Vulnerability, error)
}

View file

@ -39,6 +39,26 @@ func NewVulnerabilityProvider(reader grypeDB.VulnerabilityStoreReader) (*Vulnera
}, nil
}
func (pr *VulnerabilityProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
// note: getting a vulnerability record by id doesn't necessarily return a single record
// since records are duplicated by the set of fixes they have.
vulns, err := pr.reader.GetVulnerability(namespace, id)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace=%q pkg=%q: %w", namespace, id, err)
}
var results []vulnerability.Vulnerability
for _, vuln := range vulns {
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q): %w", vuln.Namespace, vuln.ID, err)
}
results = append(results, *vulnObj)
}
return results, nil
}
func (pr *VulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
if d == nil {
return nil, nil
@ -57,16 +77,16 @@ func (pr *VulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([
for _, n := range namespaces {
for _, packageName := range n.Resolver().Resolve(p) {
nsStr := n.String()
allPkgVulns, err := pr.reader.GetVulnerability(nsStr, packageName)
allPkgVulns, err := pr.reader.SearchForVulnerabilities(nsStr, packageName)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' pkg='%s': %w", nsStr, packageName, err)
return nil, fmt.Errorf("provider failed to search for vulnerabilities (namespace=%q pkg=%q): %w", nsStr, packageName, err)
}
for _, vuln := range allPkgVulns {
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse distro='%s': %w", d, err)
return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q distro=%q): %w", vuln.Namespace, vuln.ID, d, err)
}
vulnerabilities = append(vulnerabilities, *vulnObj)
@ -91,16 +111,16 @@ func (pr *VulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package
for _, n := range namespaces {
for _, packageName := range n.Resolver().Resolve(p) {
nsStr := n.String()
allPkgVulns, err := pr.reader.GetVulnerability(nsStr, packageName)
allPkgVulns, err := pr.reader.SearchForVulnerabilities(nsStr, packageName)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' pkg='%s': %w", nsStr, packageName, err)
return nil, fmt.Errorf("provider failed to fetch namespace=%q pkg=%q: %w", nsStr, packageName, err)
}
for _, vuln := range allPkgVulns {
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse language='%s': %w", l, err)
return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q language=%q): %w", vuln.Namespace, vuln.ID, l, err)
}
vulnerabilities = append(vulnerabilities, *vulnObj)
@ -125,9 +145,9 @@ func (pr *VulnerabilityProvider) GetByCPE(requestCPE syftPkg.CPE) ([]vulnerabili
}
for _, ns := range namespaces {
allPkgVulns, err := pr.reader.GetVulnerability(ns.String(), ns.Resolver().Normalize(requestCPE.Product))
allPkgVulns, err := pr.reader.SearchForVulnerabilities(ns.String(), ns.Resolver().Normalize(requestCPE.Product))
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' product='%s': %w", ns, requestCPE.Product, err)
return nil, fmt.Errorf("provider failed to fetch namespace=%q product=%q: %w", ns, requestCPE.Product, err)
}
normalizedRequestCPE, err := syftPkg.NewCPE(ns.Resolver().Normalize(requestCPE.BindToFmtString()))
@ -148,7 +168,7 @@ func (pr *VulnerabilityProvider) GetByCPE(requestCPE syftPkg.CPE) ([]vulnerabili
if len(candidateMatchCpes) > 0 {
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse cpe='%s': %w", requestCPE.BindToFmtString(), err)
return nil, fmt.Errorf("provider failed to inflate vulnerability record (namespace=%q id=%q cpe=%q): %w", vuln.Namespace, vuln.ID, requestCPE.BindToFmtString(), err)
}
vulnObj.CPEs = candidateMatchCpes

View file

@ -79,7 +79,19 @@ func (d *mockStore) stub() {
}
}
func (d *mockStore) GetVulnerability(namespace, name string) ([]grypeDB.Vulnerability, error) {
func (d *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
var results []grypeDB.Vulnerability
for _, vulns := range d.data[namespace] {
for _, vuln := range vulns {
if vuln.ID == id {
results = append(results, vuln)
}
}
}
return results, nil
}
func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
return d.data[namespace][name], nil
}

View file

@ -16,14 +16,12 @@ import (
syftPkg "github.com/anchore/syft/syft/pkg"
)
func TestGetByDistro(t *testing.T) {
func Test_GetByDistro(t *testing.T) {
provider, err := NewVulnerabilityProvider(newMockStore())
require.NoError(t, err)
d, err := distro.New(distro.Debian, "8", "")
if err != nil {
t.Fatalf("failed to create distro: %+v", err)
}
require.NoError(t, err)
p := pkg.Package{
ID: pkg.ID(uuid.NewString()),
@ -31,9 +29,7 @@ func TestGetByDistro(t *testing.T) {
}
actual, err := provider.GetByDistro(d, p)
if err != nil {
t.Fatalf("failed to get by distro: %+v", err)
}
require.NoError(t, err)
expected := []vulnerability.Vulnerability{
{
@ -63,7 +59,7 @@ func TestGetByDistro(t *testing.T) {
}
}
func TestGetByDistro_nilDistro(t *testing.T) {
func Test_GetByDistro_nilDistro(t *testing.T) {
provider, err := NewVulnerabilityProvider(newMockStore())
require.NoError(t, err)
@ -85,7 +81,7 @@ func must(c syftPkg.CPE, e error) syftPkg.CPE {
return c
}
func TestGetByCPE(t *testing.T) {
func Test_GetByCPE(t *testing.T) {
tests := []struct {
name string
@ -183,3 +179,30 @@ func TestGetByCPE(t *testing.T) {
}
}
func Test_Get(t *testing.T) {
provider, err := NewVulnerabilityProvider(newMockStore())
require.NoError(t, err)
actual, err := provider.Get("CVE-2014-fake-1", "debian:distro:debian:8")
require.NoError(t, err)
expected := []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
PackageQualifiers: []qualifier.Qualifier{},
CPEs: []syftPkg.CPE{},
Advisories: []vulnerability.Advisory{},
},
}
require.Len(t, actual, len(expected))
for idx, vuln := range actual {
for _, d := range deep.Equal(expected[idx], vuln) {
t.Errorf("diff: %+v", d)
}
}
}

37
grype/deprecated.go Normal file
View file

@ -0,0 +1,37 @@
package grype
import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/store"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
)
// TODO: deprecated, remove in v1.0.0
func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) {
providerConfig := pkg.ProviderConfig{
SyftProviderConfig: pkg.SyftProviderConfig{
RegistryOptions: registryOptions,
CatalogingOptions: cataloger.DefaultConfig(),
},
}
providerConfig.CatalogingOptions.Search.Scope = scopeOpt
packages, context, err := pkg.Provide(userImageStr, providerConfig)
if err != nil {
return match.Matches{}, pkg.Context{}, nil, err
}
matchers := matcher.NewDefaultMatchers(matcher.Config{})
return FindVulnerabilitiesForPackage(store, context.Distro, matchers, packages), context, packages, nil
}
// TODO: deprecated, remove in v1.0.0
func FindVulnerabilitiesForPackage(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package) match.Matches {
return matcher.FindMatches(store, d, matchers, packages)
}

View file

@ -4,77 +4,10 @@ import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/go-logger"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/store"
"github.com/anchore/grype/internal/bus"
"github.com/anchore/grype/internal/log"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
)
func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) {
providerConfig := pkg.ProviderConfig{
RegistryOptions: registryOptions,
CatalogingOptions: cataloger.DefaultConfig(),
}
providerConfig.CatalogingOptions.Search.Scope = scopeOpt
packages, context, err := pkg.Provide(userImageStr, providerConfig)
if err != nil {
return match.Matches{}, pkg.Context{}, nil, err
}
matchers := matcher.NewDefaultMatchers(matcher.Config{})
return FindVulnerabilitiesForPackage(store, context.Distro, matchers, packages), context, packages, nil
}
func FindVulnerabilitiesForPackage(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package) match.Matches {
return matcher.FindMatches(store, d, matchers, packages)
}
func LoadVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) {
dbCurator, err := db.NewCurator(cfg)
if err != nil {
return nil, nil, nil, err
}
if update {
log.Debug("looking for updates on vulnerability database")
_, err := dbCurator.Update()
if err != nil {
return nil, nil, nil, err
}
}
storeReader, dbCloser, err := dbCurator.GetStore()
if err != nil {
return nil, nil, nil, err
}
status := dbCurator.Status()
p, err := db.NewVulnerabilityProvider(storeReader)
if err != nil {
return nil, &status, nil, err
}
s := &store.Store{
Provider: p,
MetadataProvider: db.NewVulnerabilityMetadataProvider(storeReader),
ExclusionProvider: db.NewMatchExclusionProvider(storeReader),
}
closer := &db.Closer{DBCloser: dbCloser}
return s, &status, closer, nil
}
func SetLogger(logger logger.Logger) {
log.Log = logger
}

View file

@ -0,0 +1,44 @@
package grype
import (
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/store"
"github.com/anchore/grype/internal/log"
)
func LoadVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) {
dbCurator, err := db.NewCurator(cfg)
if err != nil {
return nil, nil, nil, err
}
if update {
log.Debug("looking for updates on vulnerability database")
_, err := dbCurator.Update()
if err != nil {
return nil, nil, nil, err
}
}
storeReader, dbCloser, err := dbCurator.GetStore()
if err != nil {
return nil, nil, nil, err
}
status := dbCurator.Status()
p, err := db.NewVulnerabilityProvider(storeReader)
if err != nil {
return nil, &status, nil, err
}
s := &store.Store{
Provider: p,
MetadataProvider: db.NewVulnerabilityMetadataProvider(storeReader),
ExclusionProvider: db.NewMatchExclusionProvider(storeReader),
}
closer := &db.Closer{DBCloser: dbCloser}
return s, &status, closer, nil
}

View file

@ -65,6 +65,7 @@ func (r *Matches) Add(matches ...Match) {
log.Warnf("unable to merge matches: original=%q new=%q : %w", existingMatch.String(), newMatch.String(), err)
// TODO: dropped match in this case, we should figure a way to handle this
}
r.byFingerprint[fingerprint] = existingMatch
} else {
r.byFingerprint[fingerprint] = newMatch
}

View file

@ -3,7 +3,8 @@ package apk
import (
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -29,7 +30,12 @@ type mockStore struct {
backend map[string]map[string][]grypeDB.Vulnerability
}
func (s *mockStore) GetVulnerability(namespace, name string) ([]grypeDB.Vulnerability, error) {
func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
namespaceMap := s.backend[namespace]
if namespaceMap == nil {
return nil, nil
@ -111,6 +117,7 @@ func TestSecDBOnlyMatch(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"vulnerabilityID": "CVE-2020-2",
},
Matcher: match.ApkMatcher,
},
@ -121,10 +128,7 @@ func TestSecDBOnlyMatch(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestBothSecdbAndNvdMatches(t *testing.T) {
@ -200,6 +204,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"vulnerabilityID": "CVE-2020-1",
},
Matcher: match.ApkMatcher,
},
@ -210,9 +215,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
@ -289,6 +292,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"vulnerabilityID": "CVE-2020-1",
},
Matcher: match.ApkMatcher,
},
@ -299,9 +303,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestNvdOnlyMatches(t *testing.T) {
@ -358,6 +360,7 @@ func TestNvdOnlyMatches(t *testing.T) {
Found: search.CPEResult{
CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
VersionConstraint: vulnFound.Constraint.String(),
VulnerabilityID: "CVE-2020-1",
},
Matcher: match.ApkMatcher,
},
@ -368,10 +371,7 @@ func TestNvdOnlyMatches(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestNvdMatchesWithSecDBFix(t *testing.T) {
@ -423,9 +423,7 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
@ -478,9 +476,7 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestDistroMatchBySourceIndirection(t *testing.T) {
@ -545,6 +541,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": vulnFound.Constraint.String(),
"vulnerabilityID": "CVE-2020-2",
},
Matcher: match.ApkMatcher,
},
@ -555,10 +552,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
}
assertMatches(t, expected, actual)
}
func TestNVDMatchBySourceIndirection(t *testing.T) {
@ -620,6 +614,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
Found: search.CPEResult{
CPEs: []string{vulnFound.CPEs[0].BindToFmtString()},
VersionConstraint: vulnFound.Constraint.String(),
VulnerabilityID: "CVE-2020-1",
},
Matcher: match.ApkMatcher,
},
@ -630,8 +625,17 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
actual, err := m.Match(provider, d, p)
assert.NoError(t, err)
for _, diff := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", diff)
assertMatches(t, expected, actual)
}
func assertMatches(t *testing.T, expected, actual []match.Match) {
t.Helper()
var opts = []cmp.Option{
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
}
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}

View file

@ -10,7 +10,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -19,7 +19,7 @@ type MatcherConfig struct {
func NewDotnetMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -33,7 +33,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -12,7 +12,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -21,7 +21,7 @@ type MatcherConfig struct {
func NewGolangMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -50,7 +50,7 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
}
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -46,6 +46,11 @@ type mockProvider struct {
data map[syftPkg.Language]map[string][]vulnerability.Vulnerability
}
func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func (mp *mockProvider) populateData() {
mp.data[syftPkg.Go] = map[string][]vulnerability.Vulnerability{
"istio.io/istio": {

View file

@ -1,11 +1,8 @@
package java
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
@ -21,101 +18,27 @@ const (
)
type Matcher struct {
SearchMavenUpstream bool
MavenSearcher
UseCPEs bool
cfg MatcherConfig
}
// MavenSearcher is the interface that wraps the GetMavenPackageBySha method.
type MavenSearcher interface {
// GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest
GetMavenPackageBySha(string) (*pkg.Package, error)
}
// mavenSearch implements the MavenSearcher interface
type mavenSearch struct {
client *http.Client
baseURL string
}
type mavenAPIResponse struct {
Response struct {
NumFound int `json:"numFound"`
Docs []struct {
ID string `json:"id"`
GroupID string `json:"g"`
ArtifactID string `json:"a"`
Version string `json:"v"`
P string `json:"p"`
VersionCount int `json:"versionCount"`
} `json:"docs"`
} `json:"response"`
}
func (ms *mavenSearch) GetMavenPackageBySha(sha1 string) (*pkg.Package, error) {
req, err := http.NewRequest(http.MethodGet, ms.baseURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to initialize HTTP client: %w", err)
}
q := req.URL.Query()
q.Set("q", fmt.Sprintf(sha1Query, sha1))
q.Set("rows", "1")
q.Set("wt", "json")
req.URL.RawQuery = q.Encode()
resp, err := ms.client.Do(req)
if err != nil {
return nil, fmt.Errorf("sha1 search error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %s from %s", resp.Status, req.URL.String())
}
var res mavenAPIResponse
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("json decode error: %w", err)
}
if len(res.Response.Docs) == 0 {
return nil, fmt.Errorf("digest %s: %w", sha1, errors.New("no artifact found"))
}
// artifacts might have the same SHA-1 digests.
// e.g. "javax.servlet:jstl" and "jstl:jstl"
docs := res.Response.Docs
sort.Slice(docs, func(i, j int) bool {
return docs[i].ID < docs[j].ID
})
d := docs[0]
return &pkg.Package{
Name: fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID),
Version: d.Version,
Language: syftPkg.Java,
Metadata: pkg.JavaMetadata{
PomArtifactID: d.ArtifactID,
PomGroupID: d.GroupID,
},
}, nil
type ExternalSearchConfig struct {
SearchMavenUpstream bool
MavenBaseURL string
}
type MatcherConfig struct {
SearchMavenUpstream bool
MavenBaseURL string
UseCPEs bool
ExternalSearchConfig
UseCPEs bool
}
func NewJavaMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
cfg.SearchMavenUpstream,
&mavenSearch{
cfg: cfg,
MavenSearcher: &mavenSearch{
client: http.DefaultClient,
baseURL: cfg.MavenBaseURL,
},
cfg.UseCPEs,
}
}
@ -129,7 +52,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
if m.SearchMavenUpstream {
if m.cfg.SearchMavenUpstream {
upstreamMatches, err := m.matchUpstreamMavenPackages(store, p)
if err != nil {
log.Debugf("failed to match against upstream data for %s: %v", p.Name, err)
@ -138,7 +61,7 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
}
}
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
criteriaMatches, err := search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -12,6 +12,11 @@ type mockProvider struct {
data map[syftPkg.Language]map[string][]vulnerability.Vulnerability
}
func (mp *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func (mp *mockProvider) populateData() {
mp.data[syftPkg.Java] = map[string][]vulnerability.Vulnerability{
"org.springframework.spring-webmvc": {

View file

@ -31,8 +31,13 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
},
}
matcher := Matcher{
SearchMavenUpstream: true,
MavenSearcher: newMockSearcher(p),
cfg: MatcherConfig{
ExternalSearchConfig: ExternalSearchConfig{
SearchMavenUpstream: true,
},
UseCPEs: false,
},
MavenSearcher: newMockSearcher(p),
}
store := newMockProvider()
actual, _ := matcher.matchUpstreamMavenPackages(store, p)

View file

@ -0,0 +1,88 @@
package java
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"github.com/anchore/grype/grype/pkg"
syftPkg "github.com/anchore/syft/syft/pkg"
)
// MavenSearcher is the interface that wraps the GetMavenPackageBySha method.
type MavenSearcher interface {
// GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest
GetMavenPackageBySha(string) (*pkg.Package, error)
}
// mavenSearch implements the MavenSearcher interface
type mavenSearch struct {
client *http.Client
baseURL string
}
type mavenAPIResponse struct {
Response struct {
NumFound int `json:"numFound"`
Docs []struct {
ID string `json:"id"`
GroupID string `json:"g"`
ArtifactID string `json:"a"`
Version string `json:"v"`
P string `json:"p"`
VersionCount int `json:"versionCount"`
} `json:"docs"`
} `json:"response"`
}
func (ms *mavenSearch) GetMavenPackageBySha(sha1 string) (*pkg.Package, error) {
req, err := http.NewRequest(http.MethodGet, ms.baseURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to initialize HTTP client: %w", err)
}
q := req.URL.Query()
q.Set("q", fmt.Sprintf(sha1Query, sha1))
q.Set("rows", "1")
q.Set("wt", "json")
req.URL.RawQuery = q.Encode()
resp, err := ms.client.Do(req)
if err != nil {
return nil, fmt.Errorf("sha1 search error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %s from %s", resp.Status, req.URL.String())
}
var res mavenAPIResponse
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("json decode error: %w", err)
}
if len(res.Response.Docs) == 0 {
return nil, fmt.Errorf("digest %s: %w", sha1, errors.New("no artifact found"))
}
// artifacts might have the same SHA-1 digests.
// e.g. "javax.servlet:jstl" and "jstl:jstl"
docs := res.Response.Docs
sort.Slice(docs, func(i, j int) bool {
return docs[i].ID < docs[j].ID
})
d := docs[0]
return &pkg.Package{
Name: fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID),
Version: d.Version,
Language: syftPkg.Java,
Metadata: pkg.JavaMetadata{
PomArtifactID: d.ArtifactID,
PomGroupID: d.GroupID,
},
}, nil
}

View file

@ -10,7 +10,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -19,7 +19,7 @@ type MatcherConfig struct {
func NewJavascriptMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -33,7 +33,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -114,7 +114,7 @@ func FindMatches(store interface {
packagesProcessed, vulnerabilitiesDiscovered := trackMatcher()
if defaultMatcher == nil {
defaultMatcher = &stock.Matcher{UseCPEs: true}
defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true})
}
for _, p := range packages {
packagesProcessed.N++

View file

@ -19,7 +19,12 @@ type mockStore struct {
backend map[string]map[string][]grypeDB.Vulnerability
}
func (s *mockStore) GetVulnerability(namespace, name string) ([]grypeDB.Vulnerability, error) {
func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
namespaceMap := s.backend[namespace]
if namespaceMap == nil {
return nil, nil

View file

@ -14,6 +14,11 @@ type mockProvider struct {
data map[string]map[string][]vulnerability.Vulnerability
}
func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func newMockProvider() *mockProvider {
pr := mockProvider{
data: make(map[string]map[string][]vulnerability.Vulnerability),

View file

@ -10,7 +10,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -19,7 +19,7 @@ type MatcherConfig struct {
func NewPythonMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -33,7 +33,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -16,6 +16,11 @@ type mockProvider struct {
data map[string]map[string][]vulnerability.Vulnerability
}
func (pr *mockProvider) Get(id, namespace string) ([]vulnerability.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) *mockProvider {
pr := mockProvider{
data: make(map[string]map[string][]vulnerability.Vulnerability),

View file

@ -10,7 +10,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -19,7 +19,7 @@ type MatcherConfig struct {
func NewRubyMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -33,7 +33,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -10,7 +10,7 @@ import (
)
type Matcher struct {
UseCPEs bool
cfg MatcherConfig
}
type MatcherConfig struct {
@ -19,7 +19,7 @@ type MatcherConfig struct {
func NewStockMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCPEs: cfg.UseCPEs,
cfg: cfg,
}
}
@ -33,7 +33,7 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
criteria := search.CommonCriteria
if m.UseCPEs {
if m.cfg.UseCPEs {
criteria = append(criteria, search.ByCPE)
}
return search.ByCriteria(store, d, p, m.Type(), criteria...)

View file

@ -58,10 +58,14 @@ func New(p pkg.Package) Package {
}
}
func FromCatalog(catalog *pkg.Catalog, config ProviderConfig) []Package {
result := make([]Package, 0, catalog.PackageCount())
missingCPEs := false
for _, p := range catalog.Sorted() {
func FromCatalog(catalog *pkg.Catalog, config SynthesisConfig) []Package {
return FromPackages(catalog.Sorted(), config)
}
func FromPackages(syftpkgs []pkg.Package, config SynthesisConfig) []Package {
var pkgs []Package
var missingCPEs bool
for _, p := range syftpkgs {
if len(p.CPEs) == 0 {
// For SPDX (or any format, really) we may have no CPEs
if config.GenerateMissingCPEs {
@ -71,12 +75,12 @@ func FromCatalog(catalog *pkg.Catalog, config ProviderConfig) []Package {
missingCPEs = true
}
}
result = append(result, New(p))
pkgs = append(pkgs, New(p))
}
if missingCPEs {
log.Warnf("some package(s) are missing CPEs. This may result in missing vulnerabilities. You may autogenerate these using: --add-cpes-if-none")
}
return result
return pkgs
}
// Stringer to represent a package.

View file

@ -415,7 +415,7 @@ func TestFromCatalog_DoesNotPanic(t *testing.T) {
catalog.Add(examplePackage)
assert.NotPanics(t, func() {
_ = FromCatalog(catalog, ProviderConfig{})
_ = FromCatalog(catalog, SynthesisConfig{})
})
}
@ -436,12 +436,12 @@ func TestFromCatalog_GeneratesCPEs(t *testing.T) {
})
// doesn't generate cpes when no flag
pkgs := FromCatalog(catalog, ProviderConfig{})
pkgs := FromCatalog(catalog, SynthesisConfig{})
assert.Len(t, pkgs[0].CPEs, 1)
assert.Len(t, pkgs[1].CPEs, 0)
// does generate cpes with the flag
pkgs = FromCatalog(catalog, ProviderConfig{
pkgs = FromCatalog(catalog, SynthesisConfig{
GenerateMissingCPEs: true,
})
assert.Len(t, pkgs[0].CPEs, 1)

View file

@ -6,11 +6,19 @@ import (
)
type ProviderConfig struct {
RegistryOptions *image.RegistryOptions
Exclusions []string
SyftProviderConfig
SynthesisConfig
}
type SyftProviderConfig struct {
CatalogingOptions cataloger.Config
GenerateMissingCPEs bool
RegistryOptions *image.RegistryOptions
Platform string
Exclusions []string
AttestationPublicKey string
AttestationIgnoreVerification bool
}
type SynthesisConfig struct {
GenerateMissingCPEs bool
}

View file

@ -46,8 +46,10 @@ func TestProviderLocationExcludes(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := ProviderConfig{
Exclusions: test.excludes,
CatalogingOptions: cataloger.DefaultConfig(),
SyftProviderConfig: SyftProviderConfig{
Exclusions: test.excludes,
CatalogingOptions: cataloger.DefaultConfig(),
},
}
pkgs, _, _ := Provide(test.fixture, cfg)
@ -99,8 +101,10 @@ func TestSyftLocationExcludes(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
userInput := imagetest.GetFixtureImageTarPath(t, test.fixture)
cfg := ProviderConfig{
Exclusions: test.excludes,
CatalogingOptions: cataloger.DefaultConfig(),
SyftProviderConfig: SyftProviderConfig{
Exclusions: test.excludes,
CatalogingOptions: cataloger.DefaultConfig(),
},
}
pkgs, _, err := Provide(userInput, cfg)

View file

@ -26,7 +26,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context,
return nil, Context{}, err
}
return FromCatalog(catalog, config), Context{
return FromCatalog(catalog, config.SynthesisConfig), Context{
Source: &src.Metadata,
Distro: theDistro,
}, nil

View file

@ -41,7 +41,7 @@ func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Conte
return nil, Context{}, err
}
return FromCatalog(s.Artifacts.PackageCatalog, config), Context{
return FromCatalog(s.Artifacts.PackageCatalog, config.SynthesisConfig), Context{
Source: &s.Source,
Distro: s.Artifacts.LinuxDistribution,
}, nil

View file

@ -80,7 +80,11 @@ func TestDecodeStdin(t *testing.T) {
t.Run(tt.Name, func(t *testing.T) {
f, err := os.Open(tt.Input)
require.NoError(t, err)
r, info, err := decodeStdin(f, ProviderConfig{AttestationPublicKey: tt.Key})
r, info, err := decodeStdin(f, ProviderConfig{
SyftProviderConfig: SyftProviderConfig{
AttestationPublicKey: tt.Key,
},
})
tt.WantErr(t, err)
if err == nil {
@ -88,7 +92,7 @@ func TestDecodeStdin(t *testing.T) {
sbom, format, err := syft.Decode(r)
require.NoError(t, err)
require.NotNil(t, format)
assert.Len(t, FromCatalog(sbom.Artifacts.PackageCatalog, ProviderConfig{}), tt.PkgsLen)
assert.Len(t, FromCatalog(sbom.Artifacts.PackageCatalog, SynthesisConfig{}), tt.PkgsLen)
}
})
}
@ -189,7 +193,11 @@ func TestParseAttestation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
pkgs, _, err := syftSBOMProvider(tt.Input, ProviderConfig{AttestationPublicKey: tt.Key})
pkgs, _, err := syftSBOMProvider(tt.Input, ProviderConfig{
SyftProviderConfig: SyftProviderConfig{
AttestationPublicKey: tt.Key,
},
})
tt.WantErr(t, err)
require.Len(t, pkgs, tt.PkgsLen)
})

View file

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:091c3e78-d915-4f58-b6f6-53c04d68967b",
"serialNumber": "urn:uuid:e9fe8739-7197-44bd-b5e8-050ed14af1e0",
"version": 1,
"metadata": {
"timestamp": "2022-11-23T16:23:09-05:00",
"timestamp": "2022-12-08T13:54:36-05:00",
"tools": [
{
"vendor": "anchore",
@ -19,13 +19,13 @@
},
"components": [
{
"bom-ref": "633e200f-bb1a-40fb-a14f-0adddf988155",
"bom-ref": "1759de0b-fbb0-4a8b-a085-2a2216c642b5",
"type": "library",
"name": "package-1",
"version": "1.1.1"
},
{
"bom-ref": "c60f424b-c274-4790-9209-89125f504579",
"bom-ref": "92b2e9c3-87d4-49b9-ae04-70ee075e970f",
"type": "library",
"name": "package-2",
"version": "2.2.2",
@ -67,7 +67,7 @@
},
"affects": [
{
"ref": "633e200f-bb1a-40fb-a14f-0adddf988155"
"ref": "1759de0b-fbb0-4a8b-a085-2a2216c642b5"
}
],
"properties": [
@ -100,7 +100,7 @@
},
"affects": [
{
"ref": "c60f424b-c274-4790-9209-89125f504579"
"ref": "92b2e9c3-87d4-49b9-ae04-70ee075e970f"
}
],
"properties": []

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:834850de-2bee-401f-ac47-2ad2fa642ec6" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:2a342d7a-8965-45c0-a71a-af3bcca303e3" version="1">
<metadata>
<timestamp>2022-11-23T16:23:09-05:00</timestamp>
<timestamp>2022-12-08T13:54:36-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -14,11 +14,11 @@
</component>
</metadata>
<components>
<component bom-ref="46c7e55a-84e6-49c3-ad1e-bf22bf273ea5" type="library">
<component bom-ref="1fb6b8a4-2ef6-4176-8c0d-e95dd63d5e1f" type="library">
<name>package-1</name>
<version>1.1.1</version>
</component>
<component bom-ref="d50db824-5559-46ba-8972-8f832e4e31f9" type="library">
<component bom-ref="7b81a896-f88e-40c4-bf30-1b909b028441" type="library">
<name>package-2</name>
<version>2.2.2</version>
<licenses>
@ -55,7 +55,7 @@
</analysis>
<affects>
<target>
<ref>46c7e55a-84e6-49c3-ad1e-bf22bf273ea5</ref>
<ref>1fb6b8a4-2ef6-4176-8c0d-e95dd63d5e1f</ref>
</target>
</affects>
<properties>
@ -85,7 +85,7 @@
</analysis>
<affects>
<target>
<ref>d50db824-5559-46ba-8972-8f832e4e31f9</ref>
<ref>7b81a896-f88e-40c4-bf30-1b909b028441</ref>
</target>
</affects>
<properties></properties>

View file

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:66755c26-b74b-4e0a-91cb-553fdf4e7153",
"serialNumber": "urn:uuid:58ce0fc9-d5c2-47b7-885b-4d97a3b3e335",
"version": 1,
"metadata": {
"timestamp": "2022-11-23T16:23:09-05:00",
"timestamp": "2022-12-08T13:54:36-05:00",
"tools": [
{
"vendor": "anchore",
@ -20,13 +20,13 @@
},
"components": [
{
"bom-ref": "4ab475af-df40-4a79-82fb-9c463788f56c",
"bom-ref": "52a05cbc-fb99-4571-b222-57e0ea5031b5",
"type": "library",
"name": "package-1",
"version": "1.1.1"
},
{
"bom-ref": "5c5f8e26-00bc-4d8e-9d47-0dc26c4c5fd3",
"bom-ref": "d039d2f2-00fe-46c2-959c-797f9572ac65",
"type": "library",
"name": "package-2",
"version": "2.2.2",
@ -68,7 +68,7 @@
},
"affects": [
{
"ref": "4ab475af-df40-4a79-82fb-9c463788f56c"
"ref": "52a05cbc-fb99-4571-b222-57e0ea5031b5"
}
],
"properties": [
@ -101,7 +101,7 @@
},
"affects": [
{
"ref": "5c5f8e26-00bc-4d8e-9d47-0dc26c4c5fd3"
"ref": "d039d2f2-00fe-46c2-959c-797f9572ac65"
}
],
"properties": []

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:0c4aaa8f-5858-43a6-ab4a-2c5b7856b627" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:9252cfa6-c1bb-4577-969f-6426a9576a4b" version="1">
<metadata>
<timestamp>2022-11-23T16:23:09-05:00</timestamp>
<timestamp>2022-12-08T13:54:36-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -15,11 +15,11 @@
</component>
</metadata>
<components>
<component bom-ref="3079bc39-2dc9-4ac3-8981-04e6f4175d0d" type="library">
<component bom-ref="34c838f5-21cb-4366-80ae-cc2592319a81" type="library">
<name>package-1</name>
<version>1.1.1</version>
</component>
<component bom-ref="98d49da2-7878-41c3-a55a-5841b8da967e" type="library">
<component bom-ref="19b6f279-29f9-4193-b84e-5e5bc3a32267" type="library">
<name>package-2</name>
<version>2.2.2</version>
<licenses>
@ -56,7 +56,7 @@
</analysis>
<affects>
<target>
<ref>3079bc39-2dc9-4ac3-8981-04e6f4175d0d</ref>
<ref>34c838f5-21cb-4366-80ae-cc2592319a81</ref>
</target>
</affects>
<properties>
@ -86,7 +86,7 @@
</analysis>
<affects>
<target>
<ref>98d49da2-7878-41c3-a55a-5841b8da967e</ref>
<ref>19b6f279-29f9-4193-b84e-5e5bc3a32267</ref>
</target>
</affects>
<properties></properties>

View file

@ -41,6 +41,7 @@ func TestJsonImgsPresenter(t *testing.T) {
func TestJsonDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme)
pres := NewPresenter(matches, nil, packages, context, metadataProvider, nil, nil)

View file

@ -101,7 +101,6 @@ func (pres *Presenter) Present(output io.Writer) error {
func removeDuplicateRows(items [][]string) [][]string {
seen := map[string][]string{}
//nolint:prealloc
var result [][]string
for _, v := range items {

View file

@ -33,6 +33,7 @@ func (i *CPEParameters) Merge(other CPEParameters) error {
}
type CPEResult struct {
VulnerabilityID string `json:"vulnerabilityID"`
VersionConstraint string `json:"versionConstraint"`
CPEs []string `json:"cpes"`
}
@ -124,6 +125,7 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu
},
},
Found: CPEResult{
VulnerabilityID: vuln.ID,
VersionConstraint: vuln.Constraint.String(),
CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)),
},

View file

@ -3,6 +3,7 @@ package search
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -29,6 +30,11 @@ type mockVulnStore struct {
data map[string]map[string][]grypeDB.Vulnerability
}
func (pr *mockVulnStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func newMockStore() *mockVulnStore {
pr := mockVulnStore{
data: make(map[string]map[string][]grypeDB.Vulnerability),
@ -138,7 +144,7 @@ func (pr *mockVulnStore) stub() {
}
}
func (pr *mockVulnStore) GetVulnerability(namespace, pkg string) ([]grypeDB.Vulnerability, error) {
func (pr *mockVulnStore) SearchForVulnerabilities(namespace, pkg string) ([]grypeDB.Vulnerability, error) {
return pr.data[namespace][pkg], nil
}
@ -201,6 +207,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Found: CPEResult{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
VersionConstraint: "< 3.7.6 (semver)",
VulnerabilityID: "CVE-2017-fake-1",
},
Matcher: matcher,
},
@ -250,6 +257,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Found: CPEResult{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"},
VersionConstraint: "< 3.7.6 (semver)",
VulnerabilityID: "CVE-2017-fake-1",
},
Matcher: matcher,
},
@ -282,6 +290,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Found: CPEResult{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"},
VersionConstraint: "< 3.7.4 (semver)",
VulnerabilityID: "CVE-2017-fake-2",
},
Matcher: matcher,
},
@ -326,6 +335,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Found: CPEResult{
CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"},
VersionConstraint: "= 4.0.1 (semver)",
VulnerabilityID: "CVE-2017-fake-3",
},
Matcher: matcher,
},
@ -378,6 +388,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
Found: CPEResult{
CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"},
VersionConstraint: "< 98SP3 (unknown)",
VulnerabilityID: "CVE-2017-fake-4",
},
Matcher: matcher,
},
@ -426,6 +437,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*",
},
VersionConstraint: "< 4.0 (unknown)",
VulnerabilityID: "CVE-2017-fake-5",
},
Matcher: matcher,
},
@ -484,6 +496,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
"cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*",
},
VersionConstraint: "< 1.0 (unknown)",
VulnerabilityID: "CVE-2017-fake-7",
},
Matcher: matcher,
},
@ -536,6 +549,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*",
},
VersionConstraint: "= 5.2.1 (python)",
VulnerabilityID: "CVE-2017-fake-6",
},
Matcher: matcher,
},
@ -581,6 +595,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
},
VersionConstraint: "< 4.7.7 (unknown)",
VulnerabilityID: "CVE-2021-23369",
},
Matcher: matcher,
},
@ -626,6 +641,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*",
},
VersionConstraint: "< 4.7.7 (unknown)",
VulnerabilityID: "CVE-2021-23369",
},
Matcher: matcher,
},
@ -643,7 +659,9 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
assert.NoError(t, err)
assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)
for idx, e := range test.expected {
assert.Equal(t, e.Details, actual[idx].Details)
if d := cmp.Diff(e.Details, actual[idx].Details); d != "" {
t.Errorf("unexpected match details (-want +got):\n%s", d)
}
}
})
}

View file

@ -60,6 +60,7 @@ func ByPackageDistro(store vulnerability.ProviderByDistro, d *distro.Distro, p p
"namespace": vuln.Namespace,
},
Found: map[string]interface{}{
"vulnerabilityID": vuln.ID,
"versionConstraint": vuln.Constraint.String(),
},
Confidence: 1.0, // TODO: this is hard coded for now

View file

@ -96,6 +96,7 @@ func TestFindMatchesByPackageDistro(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": "< 2014.1.5-6 (deb)",
"vulnerabilityID": "CVE-2014-fake-1",
},
Matcher: match.PythonMatcher,
},
@ -151,6 +152,7 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": "< 2014.1.5-6 (rpm)",
"vulnerabilityID": "CVE-2014-fake-4",
},
Matcher: match.PythonMatcher,
},

View file

@ -47,6 +47,7 @@ func ByPackageLanguage(store vulnerability.ProviderByLanguage, p pkg.Package, up
"namespace": vuln.Namespace,
},
Found: map[string]interface{}{
"vulnerabilityID": vuln.ID,
"versionConstraint": vuln.Constraint.String(),
},
},

View file

@ -82,6 +82,7 @@ func expectedMatch(p pkg.Package, constraint string) []match.Match {
},
Found: map[string]interface{}{
"versionConstraint": constraint,
"vulnerabilityID": "CVE-2017-fake-1",
},
Matcher: match.RubyGemMatcher,
},

View file

@ -7,6 +7,7 @@ import (
)
type Provider interface {
Get(id, namespace string) ([]Vulnerability, error)
ProviderByDistro
ProviderByLanguage
ProviderByCPE

View file

@ -0,0 +1,158 @@
package grype
import (
"strings"
"github.com/anchore/grype/grype/grypeerr"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/store"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/log"
)
type VulnerabilityMatcher struct {
Store store.Store
Matchers []matcher.Matcher
IgnoreRules []match.IgnoreRule
FailSeverity *vulnerability.Severity
NormalizeByCVE bool
}
func DefaultVulnerabilityMatcher(store store.Store) *VulnerabilityMatcher {
return &VulnerabilityMatcher{
Store: store,
Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
}
}
func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher {
m.FailSeverity = severity
return m
}
func (m *VulnerabilityMatcher) WithMatchers(matchers []matcher.Matcher) *VulnerabilityMatcher {
m.Matchers = matchers
return m
}
func (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) *VulnerabilityMatcher {
m.IgnoreRules = ignoreRules
return m
}
func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Context) (*match.Matches, []match.IgnoredMatch, error) {
var ignoredMatches []match.IgnoredMatch
matches := matcher.FindMatches(m.Store, context.Distro, m.Matchers, pkgs)
matches, ignoredMatches = m.applyIgnoreRules(matches)
if m.NormalizeByCVE {
normalizedMatches := match.NewMatches()
for originalMatch := range matches.Enumerate() {
normalizedMatches.Add(m.normalizeByCVE(originalMatch))
}
// we apply the ignore rules again in case any of the transformations done during normalization
// regresses the results (relative to the already applied ignore rules). Why do we additionally apply
// the ignore rules before normalizing? In case the user has a rule that ignores a non-normalized
// vulnerability ID, we wantMatches to ensure that the rule is honored.
matches, ignoredMatches = m.applyIgnoreRules(normalizedMatches)
}
var err error
if m.FailSeverity != nil && HasSeverityAtOrAbove(m.Store, *m.FailSeverity, matches) {
err = grypeerr.ErrAboveSeverityThreshold
}
return &matches, ignoredMatches, err
}
func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) {
var ignoredMatches []match.IgnoredMatch
if len(m.IgnoreRules) == 0 {
return matches, ignoredMatches
}
matches, ignoredMatches = match.ApplyIgnoreRules(matches, m.IgnoreRules)
if count := len(ignoredMatches); count > 0 {
log.Infof("ignoring %d matches due to user-provided ignore rules", count)
}
return matches, ignoredMatches
}
func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match {
if isCVE(match.Vulnerability.ID) {
return match
}
var effectiveCVERecordRefs []vulnerability.Reference
for _, ref := range match.Vulnerability.RelatedVulnerabilities {
if isCVE(ref.ID) {
effectiveCVERecordRefs = append(effectiveCVERecordRefs, ref)
break
}
}
switch len(effectiveCVERecordRefs) {
case 0:
// TODO: trace logging
return match
case 1:
break
default:
// TODO: trace logging
return match
}
ref := effectiveCVERecordRefs[0]
upstreamVulnRecords, err := m.Store.Get(ref.ID, ref.Namespace)
if err != nil {
log.Warnf("unable to fetch effective CVE record for id=%q namespace=%q : %v", ref.ID, ref.Namespace, err)
return match
}
switch len(upstreamVulnRecords) {
case 0:
// TODO: trace logging
return match
case 1:
break
default:
// TODO: trace logging
return match
}
originalRef := vulnerability.Reference{
ID: match.Vulnerability.ID,
Namespace: match.Vulnerability.Namespace,
}
match.Vulnerability = upstreamVulnRecords[0]
match.Vulnerability.RelatedVulnerabilities = append(match.Vulnerability.RelatedVulnerabilities, originalRef)
return match
}
func isCVE(id string) bool {
return strings.HasPrefix(strings.ToLower(id), "cve-")
}
func HasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool {
if severity == vulnerability.UnknownSeverity {
return false
}
for m := range matches.Enumerate() {
metadata, err := store.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
if err != nil {
continue
}
if vulnerability.ParseSeverity(metadata.Severity) >= severity {
return true
}
}
return false
}

View file

@ -0,0 +1,862 @@
package grype
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/grypeerr"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/matcher/ruby"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/pkg/qualifier"
"github.com/anchore/grype/grype/search"
"github.com/anchore/grype/grype/store"
"github.com/anchore/grype/grype/version"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/linux"
syftPkg "github.com/anchore/syft/syft/pkg"
)
type ack interface {
grypeDB.VulnerabilityStoreReader
grypeDB.VulnerabilityMetadataStoreReader
grypeDB.VulnerabilityMatchExclusionStoreReader
}
var _ ack = (*mockStore)(nil)
type mockStore struct {
vulnerabilities map[string]map[string][]grypeDB.Vulnerability
metadata map[string]map[string]*grypeDB.VulnerabilityMetadata
}
func (d *mockStore) GetVulnerabilityMatchExclusion(id string) ([]grypeDB.VulnerabilityMatchExclusion, error) {
//panic("implement me")
return nil, nil
}
func newMockStore() *mockStore {
d := mockStore{
vulnerabilities: make(map[string]map[string][]grypeDB.Vulnerability),
metadata: make(map[string]map[string]*grypeDB.VulnerabilityMetadata),
}
d.stub()
return &d
}
func (d *mockStore) stub() {
// METADATA /////////////////////////////////////////////////////////////////////////////////
d.metadata["CVE-2014-fake-1"] = map[string]*grypeDB.VulnerabilityMetadata{
"debian:distro:debian:8": {
Severity: "medium",
},
}
d.metadata["GHSA-2014-fake-3"] = map[string]*grypeDB.VulnerabilityMetadata{
"github:language:ruby": {
Severity: "medium",
},
}
// VULNERABILITIES ///////////////////////////////////////////////////////////////////////////
d.vulnerabilities["debian:distro:debian:8"] = map[string][]grypeDB.Vulnerability{
"neutron": {
{
PackageName: "neutron",
Namespace: "debian:distro:debian:8",
VersionConstraint: "< 2014.1.3-6",
ID: "CVE-2014-fake-1",
VersionFormat: "deb",
},
{
PackageName: "neutron",
Namespace: "debian:distro:debian:8",
VersionConstraint: "< 2013.0.2-1",
ID: "CVE-2013-fake-2",
VersionFormat: "deb",
},
},
}
d.vulnerabilities["github:language:ruby"] = map[string][]grypeDB.Vulnerability{
"activerecord": {
{
PackageName: "activerecord",
Namespace: "github:language:ruby",
VersionConstraint: "< 3.7.6",
ID: "GHSA-2014-fake-3",
VersionFormat: "unknown",
RelatedVulnerabilities: []grypeDB.VulnerabilityReference{
{
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
},
},
},
},
}
d.vulnerabilities["nvd:cpe"] = map[string][]grypeDB.Vulnerability{
"activerecord": {
{
PackageName: "activerecord",
Namespace: "nvd:cpe",
VersionConstraint: "< 3.7.6",
ID: "CVE-2014-fake-3",
VersionFormat: "unknown",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
{
PackageName: "activerecord",
Namespace: "nvd:cpe",
VersionConstraint: "< 3.7.4",
ID: "CVE-2014-fake-4",
VersionFormat: "unknown",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
},
},
{
PackageName: "activerecord",
Namespace: "nvd:cpe",
VersionConstraint: "= 4.0.1",
ID: "CVE-2014-fake-5",
VersionFormat: "unknown",
CPEs: []string{
"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*",
},
},
{
PackageName: "activerecord",
Namespace: "nvd:cpe",
VersionConstraint: "< 98SP3",
ID: "CVE-2014-fake-6",
VersionFormat: "unknown",
CPEs: []string{
"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
},
},
},
}
}
func (d *mockStore) GetVulnerabilityMetadata(id, namespace string) (*grypeDB.VulnerabilityMetadata, error) {
return d.metadata[id][namespace], nil
}
func (d *mockStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
panic("implement me")
}
func (d *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
var results []grypeDB.Vulnerability
for _, vulns := range d.vulnerabilities[namespace] {
for _, vuln := range vulns {
if vuln.ID == id {
results = append(results, vuln)
}
}
}
return results, nil
}
func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
return d.vulnerabilities[namespace][name], nil
}
func (d *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
panic("implement me")
}
func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
keys := make([]string, 0, len(d.vulnerabilities))
for k := range d.vulnerabilities {
keys = append(keys, k)
}
return keys, nil
}
func Test_HasSeverityAtOrAbove(t *testing.T) {
thePkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "the-package",
Version: "v0.1",
Type: syftPkg.RpmPkg,
}
matches := match.NewMatches()
matches.Add(match.Match{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
},
Package: thePkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
},
},
})
tests := []struct {
name string
failOnSeverity string
matches match.Matches
expectedResult bool
}{
{
name: "no-severity-set",
failOnSeverity: "",
matches: matches,
expectedResult: false,
},
{
name: "below-threshold",
failOnSeverity: "high",
matches: matches,
expectedResult: false,
},
{
name: "at-threshold",
failOnSeverity: "medium",
matches: matches,
expectedResult: true,
},
{
name: "above-threshold",
failOnSeverity: "low",
matches: matches,
expectedResult: true,
},
}
metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var failOnSeverity vulnerability.Severity
if test.failOnSeverity != "" {
sev := vulnerability.ParseSeverity(test.failOnSeverity)
if sev == vulnerability.UnknownSeverity {
t.Fatalf("could not parse severity")
}
failOnSeverity = sev
}
actual := HasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)
if test.expectedResult != actual {
t.Errorf("expected: %v got : %v", test.expectedResult, actual)
}
})
}
}
func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
mkStr := newMockStore()
vp, err := db.NewVulnerabilityProvider(mkStr)
require.NoError(t, err)
str := store.Store{
Provider: vp,
MetadataProvider: db.NewVulnerabilityMetadataProvider(mkStr),
ExclusionProvider: db.NewMatchExclusionProvider(mkStr),
}
neutron2013Pkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "neutron",
Version: "2013.1.1-1",
Type: syftPkg.DebPkg,
}
mustCPE := func(c string) syftPkg.CPE {
cp, err := syftPkg.NewCPE(c)
if err != nil {
t.Fatal(err)
}
return cp
}
activerecordPkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "activerecord",
Version: "3.7.5",
CPEs: []syftPkg.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Type: syftPkg.GemPkg,
Language: syftPkg.Ruby,
}
type fields struct {
Store store.Store
Matchers []matcher.Matcher
IgnoreRules []match.IgnoreRule
FailSeverity *vulnerability.Severity
NormalizeByCVE bool
}
type args struct {
pkgs []pkg.Package
context pkg.Context
}
tests := []struct {
name string
fields fields
args args
wantMatches match.Matches
wantIgnoredMatches []match.IgnoredMatch
wantErr error
}{
{
name: "no matches",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
},
args: args{
pkgs: []pkg.Package{
{
ID: pkg.ID(uuid.NewString()),
Name: "neutrino",
Version: "2099.1.1-1",
Type: syftPkg.DebPkg,
},
},
context: pkg.Context{
Distro: &linux.Release{
ID: "debian",
VersionID: "8",
},
},
},
},
{
name: "matches by exact-direct match (OS)",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
},
args: args{
pkgs: []pkg.Package{
neutron2013Pkg,
},
context: pkg.Context{
Distro: &linux.Release{
ID: "debian",
VersionID: "8",
},
},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
PackageQualifiers: []qualifier.Qualifier{},
CPEs: []syftPkg.CPE{},
Advisories: []vulnerability.Advisory{},
},
Package: neutron2013Pkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"distro": map[string]string{"type": "debian", "version": "8"},
"namespace": "debian:distro:debian:8",
"package": map[string]string{"name": "neutron", "version": "2013.1.1-1"},
},
Found: map[string]any{
"versionConstraint": "< 2014.1.3-6 (deb)",
"vulnerabilityID": "CVE-2014-fake-1",
},
Matcher: "dpkg-matcher",
Confidence: 1,
},
},
},
),
wantIgnoredMatches: nil,
wantErr: nil,
},
{
name: "fail on severity threshold",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
FailSeverity: func() *vulnerability.Severity {
x := vulnerability.LowSeverity
return &x
}(),
},
args: args{
pkgs: []pkg.Package{
neutron2013Pkg,
},
context: pkg.Context{
Distro: &linux.Release{
ID: "debian",
VersionID: "8",
},
},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
PackageQualifiers: []qualifier.Qualifier{},
CPEs: []syftPkg.CPE{},
Advisories: []vulnerability.Advisory{},
},
Package: neutron2013Pkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"distro": map[string]string{"type": "debian", "version": "8"},
"namespace": "debian:distro:debian:8",
"package": map[string]string{"name": "neutron", "version": "2013.1.1-1"},
},
Found: map[string]any{
"versionConstraint": "< 2014.1.3-6 (deb)",
"vulnerabilityID": "CVE-2014-fake-1",
},
Matcher: "dpkg-matcher",
Confidence: 1,
},
},
},
),
wantIgnoredMatches: nil,
wantErr: grypeerr.ErrAboveSeverityThreshold,
},
{
name: "matches by exact-direct match (language)",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
},
}),
},
args: args{
pkgs: []pkg.Package{
activerecordPkg,
},
context: pkg.Context{},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []syftPkg.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.CPEMatch,
SearchedBy: search.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Found: search.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Matcher: "ruby-gem-matcher",
Confidence: 0.9,
},
},
},
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
},
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
CPEs: []syftPkg.CPE{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"language": "ruby",
"namespace": "github:language:ruby",
},
Found: map[string]any{
"versionConstraint": "< 3.7.6 (unknown)",
"vulnerabilityID": "GHSA-2014-fake-3",
},
Matcher: "ruby-gem-matcher",
Confidence: 1,
},
},
},
),
wantIgnoredMatches: nil,
wantErr: nil,
},
{
name: "normalize by cve",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(
matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
},
},
),
NormalizeByCVE: true, // IMPORTANT!
},
args: args{
pkgs: []pkg.Package{
activerecordPkg,
},
context: pkg.Context{},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []syftPkg.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.CPEMatch,
SearchedBy: search.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Found: search.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Matcher: "ruby-gem-matcher",
Confidence: 0.9,
},
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"language": "ruby",
"namespace": "github:language:ruby",
},
Found: map[string]any{
"versionConstraint": "< 3.7.6 (unknown)",
"vulnerabilityID": "GHSA-2014-fake-3",
},
Matcher: "ruby-gem-matcher",
Confidence: 1,
},
},
},
),
wantIgnoredMatches: nil,
wantErr: nil,
},
{
name: "normalize by cve -- ignore GHSA",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(
matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
},
},
),
IgnoreRules: []match.IgnoreRule{
{
Vulnerability: "GHSA-2014-fake-3",
},
},
NormalizeByCVE: true, // IMPORTANT!
},
args: args{
pkgs: []pkg.Package{
activerecordPkg,
},
context: pkg.Context{},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []syftPkg.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.CPEMatch,
SearchedBy: search.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Found: search.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Matcher: "ruby-gem-matcher",
Confidence: 0.9,
},
},
},
),
wantErr: nil,
},
{
name: "normalize by cve -- ignore CVE",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(
matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
},
},
),
IgnoreRules: []match.IgnoreRule{
{
Vulnerability: "CVE-2014-fake-3",
},
},
NormalizeByCVE: true, // IMPORTANT!
},
args: args{
pkgs: []pkg.Package{
activerecordPkg,
},
context: pkg.Context{},
},
wantMatches: match.NewMatches(),
wantIgnoredMatches: []match.IgnoredMatch{
{
AppliedIgnoreRules: []match.IgnoreRule{
{
Vulnerability: "CVE-2014-fake-3",
},
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []syftPkg.CPE{},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
},
},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"language": "ruby",
"namespace": "github:language:ruby",
},
Found: map[string]any{
"versionConstraint": "< 3.7.6 (unknown)",
"vulnerabilityID": "GHSA-2014-fake-3",
},
Matcher: "ruby-gem-matcher",
Confidence: 1,
},
},
},
},
},
wantErr: nil,
},
{
name: "ignore CVE (not normalized by CVE)",
fields: fields{
Store: str,
Matchers: matcher.NewDefaultMatchers(matcher.Config{
Ruby: ruby.MatcherConfig{
UseCPEs: true,
},
}),
IgnoreRules: []match.IgnoreRule{
{
Vulnerability: "CVE-2014-fake-3",
},
},
},
args: args{
pkgs: []pkg.Package{
activerecordPkg,
},
},
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
},
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
CPEs: []syftPkg.CPE{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
SearchedBy: map[string]any{
"language": "ruby",
"namespace": "github:language:ruby",
},
Found: map[string]any{
"versionConstraint": "< 3.7.6 (unknown)",
"vulnerabilityID": "GHSA-2014-fake-3",
},
Matcher: "ruby-gem-matcher",
Confidence: 1,
},
},
},
),
wantIgnoredMatches: []match.IgnoredMatch{
{
AppliedIgnoreRules: []match.IgnoreRule{
{
Vulnerability: "CVE-2014-fake-3",
},
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []syftPkg.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
PackageQualifiers: []qualifier.Qualifier{},
Advisories: []vulnerability.Advisory{},
},
Package: activerecordPkg,
Details: match.Details{
{
Type: match.CPEMatch,
SearchedBy: search.CPEParameters{
Namespace: "nvd:cpe",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Found: search.CPEResult{
VulnerabilityID: "CVE-2014-fake-3",
VersionConstraint: "< 3.7.6 (unknown)",
CPEs: []string{
"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
},
},
Matcher: "ruby-gem-matcher",
Confidence: 0.9,
},
},
},
},
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &VulnerabilityMatcher{
Store: tt.fields.Store,
Matchers: tt.fields.Matchers,
IgnoreRules: tt.fields.IgnoreRules,
FailSeverity: tt.fields.FailSeverity,
NormalizeByCVE: tt.fields.NormalizeByCVE,
}
actualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
} else if err != nil {
t.Errorf("FindMatches() error = %v, wantErr %v", err, tt.wantErr)
return
}
var opts = []cmp.Option{
cmpopts.IgnoreUnexported(match.Match{}),
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
cmpopts.IgnoreUnexported(match.IgnoredMatch{}),
}
if d := cmp.Diff(tt.wantMatches.Sorted(), actualMatches.Sorted(), opts...); d != "" {
t.Errorf("FindMatches() matches mismatch [ha!] (-want +got):\n%s", d)
}
if d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != "" {
t.Errorf("FindMatches() ignored matches mismatch [ha!] (-want +got):\n%s", d)
}
})
}
}

View file

@ -55,6 +55,7 @@ type Application struct {
Log logging `yaml:"log" json:"log" mapstructure:"log"`
Attestation Attestation `yaml:"attestation" json:"attestation" mapstructure:"attestation"`
ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"`
ByCVE bool `yaml:"by-cve" json:"by-cve" mapstructure:"by-cve"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
@ -90,8 +91,6 @@ func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application
func (cfg Application) loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("check-for-app-update", true)
v.SetDefault("only-fixed", false)
v.SetDefault("only-notfixed", false)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(cfg)

View file

@ -26,15 +26,14 @@ func (cfg externalSources) loadDefaultValues(v *viper.Viper) {
v.SetDefault("external-sources.maven.base-url", defaultMavenBaseURL)
}
func (cfg externalSources) ToJavaMatcherConfig(matchCfg matcherConfig) java.MatcherConfig {
func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
// always respect if global config is disabled
smu := cfg.Maven.SearchUpstreamBySha1
if !cfg.Enable {
smu = cfg.Enable
}
return java.MatcherConfig{
return java.ExternalSearchConfig{
SearchMavenUpstream: smu,
MavenBaseURL: cfg.Maven.BaseURL,
UseCPEs: matchCfg.UseCPEs,
}
}

View file

@ -12,6 +12,11 @@ type mockStore struct {
backend map[string]map[string][]grypeDB.Vulnerability
}
func (s *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
//TODO implement me
panic("implement me")
}
func (s *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
var results []string
for k := range s.backend {
@ -148,7 +153,7 @@ func newMockDbStore() *mockStore {
},
},
"github:language:haskell": {
"ShellCheck": []grypeDB.Vulnerability{
"shellcheck": []grypeDB.Vulnerability{
{
ID: "CVE-haskell-sample",
VersionConstraint: "< 0.9.0",
@ -196,7 +201,7 @@ func newMockDbStore() *mockStore {
}
}
func (s *mockStore) GetVulnerability(namespace, name string) ([]grypeDB.Vulnerability, error) {
func (s *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
namespaceMap := s.backend[namespace]
if namespaceMap == nil {
return nil, nil

View file

@ -1,9 +1,12 @@
package integration
import (
"sort"
"strings"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype"
@ -30,9 +33,8 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["alpine:distro:alpine:3.12"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
// note: we are matching on the secdb record, not NVD primarily
@ -43,14 +45,43 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*",
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
},
"namespace": "alpine:distro:alpine:3.12",
"package": map[string]string{
"name": "libvncserver",
"version": "0.9.9",
},
},
Found: map[string]interface{}{
"cpes": []string{"cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
"constraint": "< 0.9.10 (unknown)",
"versionConstraint": "< 0.9.10 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.ApkMatcher,
},
{
// note: the input pURL has an upstream reference (redundant)
Type: "exact-indirect-match",
SearchedBy: map[string]any{
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
},
"namespace": "alpine:distro:alpine:3.12",
"package": map[string]string{
"name": "libvncserver",
"version": "0.9.9",
},
},
Found: map[string]any{
"versionConstraint": "< 0.9.10 (unknown)",
"vulnerabilityID": "CVE-alpine-libvncserver",
},
Matcher: "apk-matcher",
Confidence: 1,
},
},
})
}
@ -64,9 +95,8 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:language:javascript"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
@ -75,10 +105,12 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "javascript",
"language": "javascript",
"namespace": "github:language:javascript",
},
Found: map[string]interface{}{
"constraint": "< 3.2.1 (unknown)",
"versionConstraint": "> 5, < 7.2.1 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.JavascriptMatcher,
},
@ -99,9 +131,8 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
normalizedName := theStore.normalizedPackageNames["github:language:python"][thePkg.Name]
theVuln := theStore.backend["github:language:python"][normalizedName][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
@ -111,10 +142,12 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "python",
"language": "python",
"namespace": "github:language:python",
},
Found: map[string]interface{}{
"constraint": "< 2.6.2 (python)",
"versionConstraint": "< 2.6.2 (python)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.PythonMatcher,
},
@ -135,9 +168,8 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
normalizedName := theStore.normalizedPackageNames["github:language:dotnet"][thePkg.Name]
theVuln := theStore.backend["github:language:dotnet"][normalizedName][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
@ -147,10 +179,12 @@ func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "dotnet",
"language": "dotnet",
"namespace": "github:language:dotnet",
},
Found: map[string]interface{}{
"constraint": ">= 3.7.0.0, < 3.7.12.0 (dotnet)",
"versionConstraint": ">= 3.7.0.0, < 3.7.12.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.DotnetMatcher,
},
@ -167,9 +201,8 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:language:ruby"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
@ -179,10 +212,12 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "ruby",
"language": "ruby",
"namespace": "github:language:ruby",
},
Found: map[string]interface{}{
"constraint": "> 4.0.0, <= 4.1.1 (gemfile)",
"versionConstraint": "> 2.0.0, <= 2.1.4 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RubyGemMatcher,
},
@ -191,40 +226,53 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
}
func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/go-app")
if len(packages) != 2 {
t.Logf("Golang Packages: %+v", packages)
modPackages := catalog.PackagesByPath("/golang/go.mod")
if len(modPackages) != 1 {
t.Logf("Golang Mod Packages: %+v", modPackages)
t.Fatalf("problem with upstream syft cataloger (golang)")
}
binPackages := catalog.PackagesByPath("/go-app")
if len(binPackages) != 2 {
t.Logf("Golang Bin Packages: %+v", binPackages)
t.Fatalf("problem with upstream syft cataloger (golang)")
}
var packages []syftPkg.Package
packages = append(packages, modPackages...)
packages = append(packages, binPackages...)
for _, p := range packages {
thePkg := pkg.New(p)
theVuln := theStore.backend["github:language:go"][p.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
// no vuln match supported for main module
if p.Name == "github.com/anchore/coverage" {
continue
}
// no vuln match supported for main module
if p.Name != "github.com/anchore/coverage" {
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"langauge": "go",
},
Found: map[string]interface{}{
"constraint": " < 1.4.0 (golang)",
},
Matcher: match.GoModuleMatcher,
thePkg := pkg.New(p)
theVuln := theStore.backend["github:language:go"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "go",
"namespace": "github:language:go",
},
Found: map[string]interface{}{
"versionConstraint": "< 1.4.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.GoModuleMatcher,
},
})
}
},
})
}
}
@ -246,9 +294,8 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
theVuln := theStore.backend["github:language:java"][lookup][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
@ -257,10 +304,12 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "java",
"language": "java",
"namespace": "github:language:java",
},
Found: map[string]interface{}{
"constraint": ">= 0.0.1, < 1.2.0 (unknown)",
"versionConstraint": ">= 0.0.1, < 1.2.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.JavaMatcher,
},
@ -278,9 +327,8 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
// NOTE: this is an indirect match, in typical debian style
theVuln := theStore.backend["debian:distro:debian:8"][thePkg.Name+"-dev"][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
@ -294,9 +342,15 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
"type": "debian",
"version": "8",
},
"namespace": "debian:distro:debian:8",
"package": map[string]string{
"name": "apt-dev",
"version": "1.8.2",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.8.2 (deb)",
"versionConstraint": "<= 1.8.2 (deb)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.DpkgMatcher,
},
@ -313,9 +367,8 @@ func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["gentoo:distro:gentoo:2.8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
@ -326,11 +379,17 @@ func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "gentoo",
"version": "portage",
"version": "2.8",
},
"namespace": "gentoo:distro:gentoo:2.8",
"package": map[string]string{
"name": "app-containers/skopeo",
"version": "1.5.1",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.6.0 (gentoo)",
"versionConstraint": "< 1.6.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.PortageMatcher,
},
@ -347,9 +406,8 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
@ -363,9 +421,15 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
"type": "centos",
"version": "8",
},
"namespace": "redhat:distro:redhat:8",
"package": map[string]string{
"name": "dive",
"version": "0:0.9.2-1",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RpmMatcher,
},
@ -382,9 +446,9 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["redhat:distro:redhat:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
vulnObj.Namespace = "sles:distro:sles:12.5"
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
@ -397,9 +461,15 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata
"type": "sles",
"version": "12.5",
},
"namespace": "sles:distro:sles:12.5",
"package": map[string]string{
"name": "dive",
"version": "0:0.9.2-1",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RpmMatcher,
},
@ -414,11 +484,10 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
t.Fatalf("problem with upstream syft cataloger (haskell)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:language:haskell"][thePkg.Name][0]
theVuln := theStore.backend["github:language:haskell"][strings.ToLower(thePkg.Name)][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
@ -426,16 +495,15 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": map[string]string{
"type": "haskell",
"version": "",
},
SearchedBy: map[string]any{
"language": "haskell",
"namespace": "github:language:haskell",
},
Found: map[string]interface{}{
"constraint": "< 0.9.0 (haskell)",
Found: map[string]any{
"versionConstraint": "< 0.9.0 (unknown)",
"vulnerabilityID": "CVE-haskell-sample",
},
Matcher: match.UnknownMatcherType,
Matcher: match.StockMatcher,
},
},
})
@ -511,15 +579,11 @@ func TestMatchByImage(t *testing.T) {
userImage := "docker-archive:" + tarPath
sourceInput, err := source.ParseInput(userImage, "", true)
if err != nil {
t.Fatalf("unable to parse user input %+v", err)
}
require.NoError(t, err)
// this is purely done to help setup mocks
theSource, cleanup, err := source.New(*sourceInput, nil, nil)
if err != nil {
t.Fatalf("failed to determine image source: %+v", err)
}
require.NoError(t, err)
defer cleanup()
// TODO: relationships are not verified at this time
@ -530,73 +594,62 @@ func TestMatchByImage(t *testing.T) {
config.Catalogers = []string{"all"}
theCatalog, _, theDistro, err := syft.CatalogPackages(theSource, config)
if err != nil {
t.Fatalf("could not get the source obj: %+v", err)
}
require.NoError(t, err)
matchers := matcher.NewDefaultMatchers(matcher.Config{})
vp, err := db.NewVulnerabilityProvider(theStore)
require.NoError(t, err)
ep := db.NewMatchExclusionProvider(theStore)
store := store.Store{
str := store.Store{
Provider: vp,
MetadataProvider: nil,
ExclusionProvider: ep,
}
actualResults := grype.FindVulnerabilitiesForPackage(store, theDistro, matchers, pkg.FromCatalog(theCatalog, pkg.ProviderConfig{}))
actualResults := grype.FindVulnerabilitiesForPackage(str, theDistro, matchers, pkg.FromCatalog(theCatalog, pkg.SynthesisConfig{}))
for _, m := range actualResults.Sorted() {
for _, d := range m.Details {
observedMatchers.Add(string(d.Matcher))
}
}
// build expected matches from what's discovered from the catalog
expectedMatches := test.expectedFn(*theSource, theCatalog, theStore)
// build expected match set...
expectedMatchSet := map[string]string{}
for eMatch := range expectedMatches.Enumerate() {
// NOTE: this does not include all fields...
expectedMatchSet[eMatch.Package.Name] = eMatch.String()
}
expectedCount := len(expectedMatchSet)
// ensure that all matches are covered
actualCount := 0
for aMatch := range actualResults.Enumerate() {
actualCount++
for _, details := range aMatch.Details {
observedMatchers.Add(string(details.Matcher))
}
value, ok := expectedMatchSet[aMatch.Package.Name]
if !ok {
t.Errorf("Package: %s was expected but not found", aMatch.Package.Name)
}
if value != aMatch.String() {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(value, aMatch.String(), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
if expectedCount != actualCount {
t.Errorf("expected %d matches but got %d matches", expectedCount, actualCount)
}
assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())
})
}
// ensure that integration test cases stay in sync with the implemented matchers
observedMatchers.Remove(string(match.UnknownMatcherType))
definedMatchers.Remove(string(match.UnknownMatcherType))
observedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.MsrcMatcher))
if len(observedMatchers) != len(definedMatchers) {
t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers))
for _, m := range definedMatchers.ToSlice() {
t.Logf(" defined: %+v\n", m)
}
for _, m := range observedMatchers.ToSlice() {
t.Logf(" found: %+v\n", m)
}
defs := definedMatchers.ToSlice()
sort.Strings(defs)
obs := observedMatchers.ToSlice()
sort.Strings(obs)
t.Log(cmp.Diff(defs, obs))
}
}
func assertMatches(t *testing.T, expected, actual []match.Match) {
t.Helper()
var opts = []cmp.Option{
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
cmpopts.SortSlices(func(a, b match.Match) bool {
return a.Package.ID < b.Package.ID
}),
}
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}

View file

@ -4,7 +4,8 @@ import (
"fmt"
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -12,6 +13,7 @@ import (
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/store"
"github.com/anchore/syft/syft/source"
)
@ -43,6 +45,7 @@ func TestMatchBySBOMDocument(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": "3200970 || 878787 || base (kb)",
"vulnerabilityID": "CVE-2016-3333",
},
Matcher: match.MsrcMatcher,
Confidence: 1,
@ -62,6 +65,7 @@ func TestMatchBySBOMDocument(t *testing.T) {
},
Found: map[string]interface{}{
"versionConstraint": "< 2.0 (python)",
"vulnerabilityID": "CVE-bogus-my-package-2-python",
},
Matcher: match.StockMatcher,
Confidence: 1,
@ -72,16 +76,16 @@ func TestMatchBySBOMDocument(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mockStore := newMockDbStore()
vp, err := db.NewVulnerabilityProvider(mockStore)
mkStr := newMockDbStore()
vp, err := db.NewVulnerabilityProvider(mkStr)
require.NoError(t, err)
ep := db.NewMatchExclusionProvider(mockStore)
store := store.Store{
ep := db.NewMatchExclusionProvider(mkStr)
str := store.Store{
Provider: vp,
MetadataProvider: nil,
ExclusionProvider: ep,
}
matches, _, _, err := grype.FindVulnerabilities(store, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil)
matches, _, _, err := grype.FindVulnerabilities(str, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil)
assert.NoError(t, err)
details := make([]match.Detail, 0)
ids := strset.New()
@ -91,9 +95,14 @@ func TestMatchBySBOMDocument(t *testing.T) {
}
require.Len(t, details, len(test.expectedDetails))
cmpOpts := []cmp.Option{
cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
}
for i := range test.expectedDetails {
for _, d := range deep.Equal(test.expectedDetails[i], details[i]) {
t.Error(d)
if d := cmp.Diff(test.expectedDetails[i], details[i], cmpOpts...); d != "" {
t.Errorf("unexpected match details (-want +got):\n%s", d)
}
}

@ -1 +1 @@
Subproject commit d01404841050b2215a78ba4bbc9d996abb290a9a
Subproject commit 6ca252c622bc67e7670fe5333464400ceafbe64d