diff --git a/cmd/root.go b/cmd/root.go index 28ea60bf..d97fc56d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 -} diff --git a/cmd/root_test.go b/cmd/root_test.go index 605d58b9..70f85c15 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -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{} diff --git a/go.mod b/go.mod index e2c3c118..14d81cc1 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 840358a1..065293b9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/grype/db/v5/store/store.go b/grype/db/v5/store/store.go index 5797a6a7..72234109 100644 --- a/grype/db/v5/store/store.go +++ b/grype/db/v5/store/store.go @@ -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) diff --git a/grype/db/v5/store/store_test.go b/grype/db/v5/store/store_test.go index a43c3ea2..81f10675 100644 --- a/grype/db/v5/store/store_test.go +++ b/grype/db/v5/store/store_test.go @@ -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) { diff --git a/grype/db/v5/vulnerability_store.go b/grype/db/v5/vulnerability_store.go index 0ced8499..aa6a450c 100644 --- a/grype/db/v5/vulnerability_store.go +++ b/grype/db/v5/vulnerability_store.go @@ -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) } diff --git a/grype/db/vulnerability_provider.go b/grype/db/vulnerability_provider.go index 83da3d97..96889be4 100644 --- a/grype/db/vulnerability_provider.go +++ b/grype/db/vulnerability_provider.go @@ -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 diff --git a/grype/db/vulnerability_provider_mocks_test.go b/grype/db/vulnerability_provider_mocks_test.go index 154faa8a..06db793b 100644 --- a/grype/db/vulnerability_provider_mocks_test.go +++ b/grype/db/vulnerability_provider_mocks_test.go @@ -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 } diff --git a/grype/db/vulnerability_provider_test.go b/grype/db/vulnerability_provider_test.go index d2ba0772..ec834e3a 100644 --- a/grype/db/vulnerability_provider_test.go +++ b/grype/db/vulnerability_provider_test.go @@ -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) + } + } +} diff --git a/grype/deprecated.go b/grype/deprecated.go new file mode 100644 index 00000000..de6cd08f --- /dev/null +++ b/grype/deprecated.go @@ -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) +} diff --git a/grype/lib.go b/grype/lib.go index 5fbe7a55..25cddd9c 100644 --- a/grype/lib.go +++ b/grype/lib.go @@ -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 } diff --git a/grype/load_vulnerability_db.go b/grype/load_vulnerability_db.go new file mode 100644 index 00000000..4b1ea2cf --- /dev/null +++ b/grype/load_vulnerability_db.go @@ -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 +} diff --git a/grype/match/matches.go b/grype/match/matches.go index 950c69f9..0df9a977 100644 --- a/grype/match/matches.go +++ b/grype/match/matches.go @@ -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 } diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index b5aa6b4e..02b859ed 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -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) + } } diff --git a/grype/matcher/dotnet/matcher.go b/grype/matcher/dotnet/matcher.go index 1f04fb62..ef2af398 100644 --- a/grype/matcher/dotnet/matcher.go +++ b/grype/matcher/dotnet/matcher.go @@ -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...) diff --git a/grype/matcher/golang/matcher.go b/grype/matcher/golang/matcher.go index 7b406a7e..1e0b1778 100644 --- a/grype/matcher/golang/matcher.go +++ b/grype/matcher/golang/matcher.go @@ -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...) diff --git a/grype/matcher/golang/matcher_test.go b/grype/matcher/golang/matcher_test.go index 52eea303..58626aa2 100644 --- a/grype/matcher/golang/matcher_test.go +++ b/grype/matcher/golang/matcher_test.go @@ -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": { diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go index bcf17bc2..0bddbb42 100644 --- a/grype/matcher/java/matcher.go +++ b/grype/matcher/java/matcher.go @@ -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...) diff --git a/grype/matcher/java/matcher_mocks_test.go b/grype/matcher/java/matcher_mocks_test.go index 65dcdbbf..1913289a 100644 --- a/grype/matcher/java/matcher_mocks_test.go +++ b/grype/matcher/java/matcher_mocks_test.go @@ -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": { diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go index 52c4f4b5..84a6a80e 100644 --- a/grype/matcher/java/matcher_test.go +++ b/grype/matcher/java/matcher_test.go @@ -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) diff --git a/grype/matcher/java/maven_search.go b/grype/matcher/java/maven_search.go new file mode 100644 index 00000000..b7c37d7b --- /dev/null +++ b/grype/matcher/java/maven_search.go @@ -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 +} diff --git a/grype/matcher/javascript/matcher.go b/grype/matcher/javascript/matcher.go index 8e8eee4e..9f8d596d 100644 --- a/grype/matcher/javascript/matcher.go +++ b/grype/matcher/javascript/matcher.go @@ -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...) diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index d1185f39..5f8451db 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -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++ diff --git a/grype/matcher/msrc/matcher_test.go b/grype/matcher/msrc/matcher_test.go index 5589f322..095f6097 100644 --- a/grype/matcher/msrc/matcher_test.go +++ b/grype/matcher/msrc/matcher_test.go @@ -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 diff --git a/grype/matcher/portage/matcher_mocks_test.go b/grype/matcher/portage/matcher_mocks_test.go index 9d0bafbe..36709925 100644 --- a/grype/matcher/portage/matcher_mocks_test.go +++ b/grype/matcher/portage/matcher_mocks_test.go @@ -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), diff --git a/grype/matcher/python/matcher.go b/grype/matcher/python/matcher.go index c071e745..64057636 100644 --- a/grype/matcher/python/matcher.go +++ b/grype/matcher/python/matcher.go @@ -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...) diff --git a/grype/matcher/rpm/matcher_mocks_test.go b/grype/matcher/rpm/matcher_mocks_test.go index 7ebb13ab..f8dfcad9 100644 --- a/grype/matcher/rpm/matcher_mocks_test.go +++ b/grype/matcher/rpm/matcher_mocks_test.go @@ -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), diff --git a/grype/matcher/ruby/matcher.go b/grype/matcher/ruby/matcher.go index a2c934ac..2a1840c1 100644 --- a/grype/matcher/ruby/matcher.go +++ b/grype/matcher/ruby/matcher.go @@ -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...) diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go index 4e409738..7f30a52d 100644 --- a/grype/matcher/stock/matcher.go +++ b/grype/matcher/stock/matcher.go @@ -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...) diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 96a39999..4f6fa5aa 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -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. diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 329a71aa..9332afae 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -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) diff --git a/grype/pkg/provider_config.go b/grype/pkg/provider_config.go index da7a0e87..e1102579 100644 --- a/grype/pkg/provider_config.go +++ b/grype/pkg/provider_config.go @@ -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 +} diff --git a/grype/pkg/provider_test.go b/grype/pkg/provider_test.go index c3cc45d3..01e66716 100644 --- a/grype/pkg/provider_test.go +++ b/grype/pkg/provider_test.go @@ -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) diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index e61fa099..c6a73a85 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -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 diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go index a9135884..5c4b2bf8 100644 --- a/grype/pkg/syft_sbom_provider.go +++ b/grype/pkg/syft_sbom_provider.go @@ -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 diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go index e23542e6..013c6187 100644 --- a/grype/pkg/syft_sbom_provider_test.go +++ b/grype/pkg/syft_sbom_provider_test.go @@ -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) }) diff --git a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_json.golden b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_json.golden index 1d52ee20..66facd46 100644 --- a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_json.golden +++ b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_json.golden @@ -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": [] diff --git a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_xml.golden b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_xml.golden index 6a219d3d..7584af38 100644 --- a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_xml.golden +++ b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterDir_xml.golden @@ -1,7 +1,7 @@ - + - 2022-11-23T16:23:09-05:00 + 2022-12-08T13:54:36-05:00 anchore @@ -14,11 +14,11 @@ - + package-1 1.1.1 - + package-2 2.2.2 @@ -55,7 +55,7 @@ - 46c7e55a-84e6-49c3-ad1e-bf22bf273ea5 + 1fb6b8a4-2ef6-4176-8c0d-e95dd63d5e1f @@ -85,7 +85,7 @@ - d50db824-5559-46ba-8972-8f832e4e31f9 + 7b81a896-f88e-40c4-bf30-1b909b028441 diff --git a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_json.golden b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_json.golden index 8ba007ea..701239c0 100644 --- a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_json.golden +++ b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_json.golden @@ -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": [] diff --git a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_xml.golden b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_xml.golden index a9f09a34..09ba7b7b 100644 --- a/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_xml.golden +++ b/grype/presenter/cyclonedxvex/test-fixtures/snapshot/TestCycloneDxPresenterImage_xml.golden @@ -1,7 +1,7 @@ - + - 2022-11-23T16:23:09-05:00 + 2022-12-08T13:54:36-05:00 anchore @@ -15,11 +15,11 @@ - + package-1 1.1.1 - + package-2 2.2.2 @@ -56,7 +56,7 @@ - 3079bc39-2dc9-4ac3-8981-04e6f4175d0d + 34c838f5-21cb-4366-80ae-cc2592319a81 @@ -86,7 +86,7 @@ - 98d49da2-7878-41c3-a55a-5841b8da967e + 19b6f279-29f9-4193-b84e-5e5bc3a32267 diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index 60fac514..07ddf944 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -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) diff --git a/grype/presenter/table/presenter.go b/grype/presenter/table/presenter.go index 3bbdf173..2c114a66 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -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 { diff --git a/grype/search/cpe.go b/grype/search/cpe.go index 68627602..50cb3a6f 100644 --- a/grype/search/cpe.go +++ b/grype/search/cpe.go @@ -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)), }, diff --git a/grype/search/cpe_test.go b/grype/search/cpe_test.go index c441a33d..de9241ff 100644 --- a/grype/search/cpe_test.go +++ b/grype/search/cpe_test.go @@ -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) + } } }) } diff --git a/grype/search/distro.go b/grype/search/distro.go index 644fbe12..b4720612 100644 --- a/grype/search/distro.go +++ b/grype/search/distro.go @@ -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 diff --git a/grype/search/distro_test.go b/grype/search/distro_test.go index 5b3f8fda..fecc7f78 100644 --- a/grype/search/distro_test.go +++ b/grype/search/distro_test.go @@ -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, }, diff --git a/grype/search/language.go b/grype/search/language.go index d929a8f7..8632a8b8 100644 --- a/grype/search/language.go +++ b/grype/search/language.go @@ -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(), }, }, diff --git a/grype/search/language_test.go b/grype/search/language_test.go index 147aeff7..e2574686 100644 --- a/grype/search/language_test.go +++ b/grype/search/language_test.go @@ -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, }, diff --git a/grype/vulnerability/provider.go b/grype/vulnerability/provider.go index 73fe05bd..fa3ed3a7 100644 --- a/grype/vulnerability/provider.go +++ b/grype/vulnerability/provider.go @@ -7,6 +7,7 @@ import ( ) type Provider interface { + Get(id, namespace string) ([]Vulnerability, error) ProviderByDistro ProviderByLanguage ProviderByCPE diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go new file mode 100644 index 00000000..766599e8 --- /dev/null +++ b/grype/vulnerability_matcher.go @@ -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 +} diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go new file mode 100644 index 00000000..6d4c376c --- /dev/null +++ b/grype/vulnerability_matcher_test.go @@ -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) + } + }) + } +} diff --git a/internal/config/application.go b/internal/config/application.go index 5731fd26..bb1594e8 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -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) diff --git a/internal/config/datasources.go b/internal/config/datasources.go index e875655d..df0dc3c1 100644 --- a/internal/config/datasources.go +++ b/internal/config/datasources.go @@ -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, } } diff --git a/test/integration/db_mock_test.go b/test/integration/db_mock_test.go index 9623a5eb..8aa753cb 100644 --- a/test/integration/db_mock_test.go +++ b/test/integration/db_mock_test.go @@ -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 diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 30c77891..f761211b 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -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) + } +} diff --git a/test/integration/match_by_sbom_document_test.go b/test/integration/match_by_sbom_document_test.go index 113f34af..ea65d10e 100644 --- a/test/integration/match_by_sbom_document_test.go +++ b/test/integration/match_by_sbom_document_test.go @@ -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) } } diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index d0140484..6ca252c6 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit d01404841050b2215a78ba4bbc9d996abb290a9a +Subproject commit 6ca252c622bc67e7670fe5333464400ceafbe64d