From a869480f89c07c206eed720dbede9c69ba4d2f19 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 8 Dec 2022 15:22:40 -0500 Subject: [PATCH 1/5] Optionally orient results by CVE (#1020) Co-authored-by: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips --- cmd/root.go | 150 +-- cmd/root_test.go | 110 --- go.mod | 2 +- go.sum | 4 +- grype/db/v5/store/store.go | 22 +- grype/db/v5/store/store_test.go | 2 +- grype/db/v5/vulnerability_store.go | 6 +- grype/db/vulnerability_provider.go | 38 +- grype/db/vulnerability_provider_mocks_test.go | 14 +- grype/db/vulnerability_provider_test.go | 41 +- grype/deprecated.go | 37 + grype/lib.go | 67 -- grype/load_vulnerability_db.go | 44 + grype/match/matches.go | 1 + grype/matcher/apk/matcher_test.go | 60 +- grype/matcher/dotnet/matcher.go | 6 +- grype/matcher/golang/matcher.go | 6 +- grype/matcher/golang/matcher_test.go | 5 + grype/matcher/java/matcher.go | 97 +- grype/matcher/java/matcher_mocks_test.go | 5 + grype/matcher/java/matcher_test.go | 9 +- grype/matcher/java/maven_search.go | 88 ++ grype/matcher/javascript/matcher.go | 6 +- grype/matcher/matchers.go | 2 +- grype/matcher/msrc/matcher_test.go | 7 +- grype/matcher/portage/matcher_mocks_test.go | 5 + grype/matcher/python/matcher.go | 6 +- grype/matcher/rpm/matcher_mocks_test.go | 5 + grype/matcher/ruby/matcher.go | 6 +- grype/matcher/stock/matcher.go | 6 +- grype/pkg/package.go | 16 +- grype/pkg/package_test.go | 6 +- grype/pkg/provider_config.go | 14 +- grype/pkg/provider_test.go | 12 +- grype/pkg/syft_provider.go | 2 +- grype/pkg/syft_sbom_provider.go | 2 +- grype/pkg/syft_sbom_provider_test.go | 14 +- .../TestCycloneDxPresenterDir_json.golden | 12 +- .../TestCycloneDxPresenterDir_xml.golden | 12 +- .../TestCycloneDxPresenterImage_json.golden | 12 +- .../TestCycloneDxPresenterImage_xml.golden | 12 +- grype/presenter/json/presenter_test.go | 1 + grype/presenter/table/presenter.go | 1 - grype/search/cpe.go | 2 + grype/search/cpe_test.go | 22 +- grype/search/distro.go | 1 + grype/search/distro_test.go | 2 + grype/search/language.go | 1 + grype/search/language_test.go | 1 + grype/vulnerability/provider.go | 1 + grype/vulnerability_matcher.go | 158 ++++ grype/vulnerability_matcher_test.go | 862 ++++++++++++++++++ internal/config/application.go | 3 +- internal/config/datasources.go | 5 +- test/integration/db_mock_test.go | 9 +- test/integration/match_by_image_test.go | 327 ++++--- .../match_by_sbom_document_test.go | 25 +- test/quality/vulnerability-match-labels | 2 +- 58 files changed, 1781 insertions(+), 613 deletions(-) create mode 100644 grype/deprecated.go create mode 100644 grype/load_vulnerability_db.go create mode 100644 grype/matcher/java/maven_search.go create mode 100644 grype/vulnerability_matcher.go create mode 100644 grype/vulnerability_matcher_test.go diff --git a/cmd/root.go b/cmd/root.go index 28ea60bf723..d97fc56d96b 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) - - bus.Publish(partybus.Event{ - Type: event.AppUpdateAvailable, - Value: newVersion, - }) - } else { - log.Debugf("No new %s update available", internal.ApplicationName) - } - } + checkForAppUpdate() - 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) - - 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) + applyDistroHint(packages, &pkgContext, appConfig) - 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 605d58b99f3..70f85c159a6 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 e2c3c118e69..14d81cc1773 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 840358a12ff..065293b9f15 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 5797a6a74a7..7223410986a 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 a43c3ea229e..81f10675951 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 0ced84996af..aa6a450c2d9 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 83da3d97c6c..96889be428e 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 154faa8a741..06db793b469 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 d2ba0772f41..ec834e3a374 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 00000000000..de6cd08f89c --- /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 5fbe7a55b99..25cddd9c5ad 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 00000000000..4b1ea2cf9e1 --- /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 950c69f9a51..0df9a977884 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 b5aa6b4eaeb..02b859edd40 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 1f04fb6287a..ef2af39815d 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 7b406a7ecc5..1e0b17786a3 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 52eea303744..58626aa28be 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 bcf17bc2c98..0bddbb42c4f 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 -} - -// 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"` + cfg MatcherConfig } -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 65dcdbbfda0..1913289a7c7 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 52c4f4b5017..84a6a80e245 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 00000000000..b7c37d7bf05 --- /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 8e8eee4ebe1..9f8d596dc69 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 d1185f393f3..5f8451db733 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 5589f322106..095f6097bf9 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 9d0bafbefe4..367099255fd 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 c071e745179..64057636216 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 7ebb13ab75e..f8dfcad9881 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 a2c934ac0f5..2a1840c1e5d 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 4e409738c18..7f30a52df9a 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 96a39999c3c..4f6fa5aa75c 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 329a71aaba6..9332afae575 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 da7a0e87265..e11025798d8 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 c3cc45d351b..01e667168cd 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 e61fa09926c..c6a73a85e31 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 a9135884378..5c4b2bf887e 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 e23542e6d2d..013c6187efb 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 1d52ee204a7..66facd46dc9 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 6a219d3d554..7584af380b2 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 8ba007ea2f4..701239c01f4 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 a9f09a34e30..09ba7b7b1ec 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 60fac5144ab..07ddf9446f8 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 3bbdf173905..2c114a66008 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 68627602356..50cb3a6f83e 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 c441a33d5fc..de9241ff5d6 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 644fbe120d1..b4720612d85 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 5b3f8fda6a8..fecc7f783a9 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 d929a8f7618..8632a8b8baa 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 147aeff7783..e2574686722 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 73fe05bd28f..fa3ed3a7935 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 00000000000..766599e8278 --- /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 00000000000..6d4c376c5ed --- /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 5731fd26177..bb1594e8771 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 e875655decd..df0dc3c1700 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 9623a5eb87c..8aa753cb456 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 30c778914a2..f761211b2f3 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{})) - - // 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) + actualResults := grype.FindVulnerabilitiesForPackage(str, theDistro, matchers, pkg.FromCatalog(theCatalog, pkg.SynthesisConfig{})) - // 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)) + for _, m := range actualResults.Sorted() { + for _, d := range m.Details { + observedMatchers.Add(string(d.Matcher)) } } - if expectedCount != actualCount { - t.Errorf("expected %d matches but got %d matches", expectedCount, actualCount) - } + // build expected matches from what's discovered from the catalog + expectedMatches := test.expectedFn(*theSource, theCatalog, theStore) + + 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 113f34af702..ea65d10e343 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 d0140484105..6ca252c622b 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit d01404841050b2215a78ba4bbc9d996abb290a9a +Subproject commit 6ca252c622bc67e7670fe5333464400ceafbe64d From 2ace4c0b117ed939a150e324d364a220ec44a8d8 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 12 Dec 2022 15:59:25 -0500 Subject: [PATCH 2/5] docs: update quality gate docs (#1032) --- test/quality/README.md | 182 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/test/quality/README.md b/test/quality/README.md index 5acb0dac13f..59caeb87282 100644 --- a/test/quality/README.md +++ b/test/quality/README.md @@ -14,6 +14,8 @@ static set of reference container images. The kinds of comparisons made are: ## Getting started +For information about required setup see: [Required setup](#required-setup). + To capture raw tool output and store into the local `.yardstick` directory for further analysis: ``` @@ -138,3 +140,183 @@ to keep in mind: Pushing this ceiling will likely raise the number of unlabled matches significantly for all images. Only bump this ceiling if all possible matches are labeled. + +## Workflow + +One way of working is to simply run `yardstick` and `gate.py` in the `test/quality` directory. +You will need to make sure the `vulnerabilty-match-labels` submodule has been initialized. This happens automatically +for some `make` commands, but you can ensure this by `git submodule update --init`. After the submodule has been +initialized, the match data from `vulnerabilty-match-labels` will be available locally. + +**TIP**: when dealing with submodules, it may be convenient to set the git config option `submodule.recurse` to `true` +so `git checkout` will automatically update submodules to the correct commit: +```shell +git config submodule.recurse true +``` + +To do this we need some results to begin with. As noted above, start with (this does ensure the submodule is initialized): +```shell +make capture +``` + +This will download prebuilt SBOMs for the configured images and generate match results for configured tools (here: +the previous Grype version as well as the local version). + +After `make capture` has finished, we should have results and can now start inspecting and +modifying the comparison labels. + +To get started, let's assume we see some quality gate failure in like this (something found in CI +or after running `./gate.py`): +``` +Running comparison against labels... + Results used: + ├── f4fb4e6e-c911-41b6-9a10-f90b3954a41a : grype@v0.53.1-19-g8900767 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 + └── fcebdd0b-d80a-4fe2-b81a-802c7b98d83b : grype@v0.53.1 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 + +Match differences between tooling (with labels): + TOOL PARTITION PACKAGE VULNERABILITY LABEL COMMENTARY + grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44531 TruePositive (this is a new FN 😱) + grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44532 TruePositive (this is a new FN 😱) + grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44533 TruePositive (this is a new FN 😱) + +Failed quality gate + - current F1 score is lower than the latest release F1 score: current=0.80 latest=0.80 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 + - current indeterminate matches % is greater than 10%: current=13.60% image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 + - current false negatives is greater than the latest release false negatives: current=6 latest=3 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 +``` + +This tells us some important information: which package, version, and vulnerability had a difference; +how it was previously labeled, and most importantly: the image we need to focus on (`docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9`). + +Using the SHA above, we can run `yardstick` to see which results are available: +```shell +$ yardstick result list --result-set pr_vs_latest_via_sbom | grep 808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 + +5bf0611b-183f-4525-a1ab-f268f62f48b6 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 grype@v0.53.1 2022-12-09 20:49:56+00:00 +43a9650a-d5de-4687-b3ba-459105e32cb8 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 grype@v0.53.1-15-gf29a32b 2022-12-09 20:49:53+00:00 +67913f57-690f-4f35-a2d9-ffccd2a0b2a1 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 syft@v0.60.1 2022-11-01 20:30:52+00:00 +``` + +We'll need to use the UUIDs to explore the labels, so copy the first UUID, which we can see was run against the last Grype release (`grype@v0.53.1`). Use the UUID to explore and edit the results with +`yardstick label explore`: +```shell +yardstick label explore 5bf0611b-183f-4525-a1ab-f268f62f48b6 +``` + +At this point we can use the TUI to explore and modify the match data, by deleting things or labeling as +true positives, false positives, etc.. **After making changes make sure to save the results** (`Ctrl-S`)! + +At this point you can run the quality gate using updated label data. The quality gate can run against +just one image, for example the image we first found in the failure, so run the quality gate and see +how changes to the label data have affected the result: +```shell +./gate.py --image docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 +``` + +After iterating on all the changes we need using `yardstick label explore`, we're now ready to commit changes. Since +we're using `git submodules`, we need to do two steps: +1. get the changes merged to the `vulnerability-match-labels` repository `main` branch +2. update the submodule in this repository + +To create a pull request for the `vulnerability-match-labels`, make sure you are in the `vulnerability-match-labels` +subdirectory and create a branch -- something like: +```shell +git checkout --no-track -b my-branch-name +``` + +Commit the changes to this branch, push, create a pull request like normal. NOTE: you may need to add a fork +(`git remote add ...`) and push to the fork if you don't have push permissions against the main +`vulnerability-match-labels` repo. After the PR is approved and merged to `vulnerability-match-labels` repo's `main` +branch, update the submodule locally using: +```shell +git submodule update --remote +``` + +Next, _commit the submodule change_ as part of any other changes +to the Grype pull request and push as part of the in-progress PR +against Grype. The PR will now use the updated match labels when running +the quality check. + +## Required setup + +In order to manage Python versions, [pyenv](https://github.com/pyenv/pyenv) can be used. (e.g. `brew install pyenv`) + +Both this project and `yardstick` require Python 3.10. + +Using `pyenv`, see which python versions are available, for example: +```shell +$ pyenv install --list|grep 3.10 + 3.10.0 + ... + 3.10.7 + ... +``` + +In this case, we see `3.10.7` is the latest version, so we'll use that for the rest of the setup: + +Install this version using `pyenv`: +```shell +pyenv install 3.10.7 +``` + +NOTE: to view the specific Python versions installed use `pyenv versions`: +```shell +$ pyenv versions + system +* 3.8.13 (set by /Users/usr/.pyenv/version) + 3.10.7 +``` + +To select the `3.10` version use the exact version number: +```shell +pyenv shell 3.10.7 +``` + +(or maybe just: `pyenv shell $(pyenv versions | grep 3.10 | tail -1)`) + +Verify this has worked properly by running: +```shell +python --version +``` + +**Important:** it is also required to have `oras` installed (e.g. `brew install oras`) + +**After** setting the working Python version to 3.10, in the `test/quality` directory, +you need to set up a virtual environment using: +```shell +make venv +``` + +**After** creating the virtual environment, you can now activate it to set up a +working shell using: +```shell +. venv/bin/activate +``` + +You should now have a shell running in the correct virtual environment, it might look something +like this: +```shell +(venv) user@HOST quality % +``` + +Now you should be able to run both `yardstick` and `./gate.py`. + +## Troubleshooting + +As noted above, yardstick requires Python 3.10. If you try to run with an older version, such as +the default macOS 3.8 version, you will likely see an error similar to: + +``` +Traceback (most recent call last): + File "./vulnerability-match-labels/sboms.py", line 12, in + import yardstick + File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/__init__.py", line 4, in + from . import arrange, artifact, capture, cli, comparison, label, store, tool, utils + File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/arrange.py", line 4, in + from yardstick import artifact + File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py", line 482, in + class ResultSet: + File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py", line 484, in ResultSet + state: list[ResultState] = field(default_factory=list) +TypeError: 'type' object is not subscriptable +``` From 2c94031e1e5911ecce95f1bca86ddff50a342c80 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 12 Dec 2022 15:59:47 -0500 Subject: [PATCH 3/5] fix: Exclude binary packages that have overlap by file ownership relationship (#1024) --- .gitignore | 3 ++ grype/pkg/package.go | 20 +++++++ grype/pkg/package_test.go | 72 +++++++++++++++++++++++++ grype/pkg/syft_provider.go | 4 +- grype/pkg/syft_sbom_provider.go | 5 +- test/quality/.python-version | 1 + test/quality/vulnerability-match-labels | 2 +- 7 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 test/quality/.python-version diff --git a/.gitignore b/.gitignore index f2d42c4e4fb..a4e51f36a27 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ CHANGELOG.md *.tmp coverage.txt +# OS files +.DS_Store + # Binaries for programs and plugins *.exe *.exe~ diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 4f6fa5aa75c..5662a833227 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -6,6 +6,7 @@ import ( "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" "github.com/anchore/syft/syft/source" @@ -88,6 +89,25 @@ func (p Package) String() string { return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s, upstreams=%d)", p.Type, p.Name, p.Version, len(p.Upstreams)) } +func RemoveBinaryPackagesByOverlap(catalog *pkg.Catalog, relationships []artifact.Relationship) *pkg.Catalog { + byOverlap := map[artifact.ID]artifact.Identifiable{} + for _, r := range relationships { + if r.Type == artifact.OwnershipByFileOverlapRelationship { + byOverlap[r.To.ID()] = r.To + } + } + + out := pkg.NewCatalog() + for p := range catalog.Enumerate() { + if _, ok := byOverlap[p.ID()]; p.Type == pkg.BinaryPkg && ok { + continue + } + out.Add(p) + } + + return out +} + func dataFromPkg(p pkg.Package) (MetadataType, interface{}, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 9332afae575..46f77f67ffc 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -7,6 +7,7 @@ import ( "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" syftFile "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" @@ -486,3 +487,74 @@ func Test_getNameAndELVersion(t *testing.T) { func intRef(i int) *int { return &i } + +func Test_RemoveBinaryPackagesByOverlap(t *testing.T) { + tests := []struct { + name string + sbom catalogRelationships + expectedPackages []string + }{ + { + name: "includes all packages without overlap", + sbom: catalogWithBinaryOverlaps([]string{"go"}, []string{}), + expectedPackages: []string{"go"}, + }, + { + name: "excludes single package by overlap", + sbom: catalogWithBinaryOverlaps([]string{"go", "node"}, []string{"node"}), + expectedPackages: []string{"go"}, + }, + { + name: "excludes multiple package by overlap", + sbom: catalogWithBinaryOverlaps([]string{"go", "node", "python", "george"}, []string{"node", "george"}), + expectedPackages: []string{"go", "python"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + catalog := RemoveBinaryPackagesByOverlap(test.sbom.catalog, test.sbom.relationships) + pkgs := FromCatalog(catalog, SynthesisConfig{}) + var pkgNames []string + for _, p := range pkgs { + pkgNames = append(pkgNames, p.Name) + } + assert.EqualValues(t, test.expectedPackages, pkgNames) + }) + } +} + +type catalogRelationships struct { + catalog *syftPkg.Catalog + relationships []artifact.Relationship +} + +func catalogWithBinaryOverlaps(packages []string, overlaps []string) catalogRelationships { + var pkgs []syftPkg.Package + var relationships []artifact.Relationship + + for _, name := range packages { + p := syftPkg.Package{ + Name: name, + Type: syftPkg.BinaryPkg, + } + p.SetID() + + for _, overlap := range overlaps { + if overlap == name { + relationships = append(relationships, artifact.Relationship{ + From: p, + To: p, + Type: artifact.OwnershipByFileOverlapRelationship, + }) + } + } + + pkgs = append(pkgs, p) + } + catalog := syftPkg.NewCatalog(pkgs...) + + return catalogRelationships{ + catalog: catalog, + relationships: relationships, + } +} diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index c6a73a85e31..a2bae0cf0a3 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -21,11 +21,13 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, } defer cleanup() - catalog, _, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) + catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) if err != nil { return nil, Context{}, err } + catalog = RemoveBinaryPackagesByOverlap(catalog, relationships) + return FromCatalog(catalog, config.SynthesisConfig), Context{ Source: &src.Metadata, Distro: theDistro, diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go index 5c4b2bf887e..06f49897069 100644 --- a/grype/pkg/syft_sbom_provider.go +++ b/grype/pkg/syft_sbom_provider.go @@ -41,7 +41,10 @@ func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Conte return nil, Context{}, err } - return FromCatalog(s.Artifacts.PackageCatalog, config.SynthesisConfig), Context{ + catalog := s.Artifacts.PackageCatalog + catalog = RemoveBinaryPackagesByOverlap(catalog, s.Relationships) + + return FromCatalog(catalog, config.SynthesisConfig), Context{ Source: &s.Source, Distro: s.Artifacts.LinuxDistribution, }, nil diff --git a/test/quality/.python-version b/test/quality/.python-version new file mode 100644 index 00000000000..1281604a491 --- /dev/null +++ b/test/quality/.python-version @@ -0,0 +1 @@ +3.10.7 diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index 6ca252c622b..c2876a52c5f 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit 6ca252c622bc67e7670fe5333464400ceafbe64d +Subproject commit c2876a52c5f876bdf05439b46541f8a7a67b1f7c From 93499eec7e3ce2704755e9f51457181b06b519c5 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 19:30:04 -0500 Subject: [PATCH 4/5] Update Syft to v0.63.0 (#1037) --- go.mod | 10 +++++----- go.sum | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 14d81cc1773..797b26398d4 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/anchore/grype go 1.18 require ( - github.com/CycloneDX/cyclonedx-go v0.6.0 + github.com/CycloneDX/cyclonedx-go v0.7.0 github.com/Masterminds/sprig/v3 v3.2.2 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.3.3 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7 - github.com/anchore/stereoscope v0.0.0-20221130153459-3b80d983223f + github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/docker/docker v20.10.17+incompatible github.com/dustin/go-humanize v1.0.0 @@ -53,7 +53,7 @@ require ( require ( github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 - github.com/anchore/syft v0.62.3 + github.com/anchore/syft v0.63.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/in-toto/in-toto-golang v0.4.1-0.20221018183522-731d0640b65f github.com/mitchellh/mapstructure v1.5.0 @@ -95,7 +95,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect - github.com/containerd/containerd v1.6.8 // indirect + github.com/containerd/containerd v1.6.12 // indirect github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect @@ -181,7 +181,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect diff --git a/go.sum b/go.sum index 065293b9f15..4c09c04125d 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.6.0 h1:SizWGbZzFTC/O/1yh072XQBMxfvsoWqd//oKCIyzFyE= -github.com/CycloneDX/cyclonedx-go v0.6.0/go.mod h1:nQCiF4Tvrg5Ieu8qPhYMvzPGMu5I7fANZkrSsJjl5mg= +github.com/CycloneDX/cyclonedx-go v0.7.0 h1:jNxp8hL7UpcvPDFXjY+Y1ibFtsW+e5zyF9QoSmhK/zg= +github.com/CycloneDX/cyclonedx-go v0.7.0/go.mod h1:W5Z9w8pTTL+t+yG3PCiFRGlr8PUlE0pGWzKSJbsyXkg= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= @@ -190,7 +190,7 @@ github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwT github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= -github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.5/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -238,10 +238,10 @@ github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7 h1:kDrYkTS github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJoNR15ZuFJ63qwBMqrmGIt/7VsBhLF8= github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc= -github.com/anchore/stereoscope v0.0.0-20221130153459-3b80d983223f h1:JB4foD0R//XJ6oAtjK9h9ABjHhmzUAakjQRKMckPhnA= -github.com/anchore/stereoscope v0.0.0-20221130153459-3b80d983223f/go.mod h1:Oa0EpewvxiynI8zxLGj2SZ6gSoGtBPQrbZNBrYNBsvE= -github.com/anchore/syft v0.62.3 h1:2D5J2oeGIJ3BtIofRllxww4EdAv/dykekrF6zScanJY= -github.com/anchore/syft v0.62.3/go.mod h1:QIZSl6B5mb+o6Rorz547sAWSRhLjKzNtTNXuO10udZU= +github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1 h1:DXUAm/H9chRTEzMfkFyduBIcCiJyFXhCmv3zH3C0HGs= +github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1/go.mod h1:/zjVnu2Jdl7xQCUtASegzeEg+IHKrM7SyMqdao3e+Nc= +github.com/anchore/syft v0.63.0 h1:L00jzHH7pqX1oLsHGAQTaI3162UKfNoyGDvlwOaqb3c= +github.com/anchore/syft v0.63.0/go.mod h1:VEm67LKIGewP1FLoameSlVQocozvmKFlpaljEPhBSQg= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= @@ -348,8 +348,7 @@ github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/bradleyjkemp/cupaloy/v2 v2.7.0 h1:AT0vOjO68RcLyenLCHOGZzSNiuto7ziqzq6Q1/3xzMQ= -github.com/bradleyjkemp/cupaloy/v2 v2.7.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= @@ -458,8 +457,8 @@ github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTV github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= -github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= -github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= +github.com/containerd/containerd v1.6.12 h1:kJ9b3mOFKf8yqo05Ob+tMoxvt1pbVWhnB0re9Y+k+8c= +github.com/containerd/containerd v1.6.12/go.mod h1:K4Bw7gjgh4TnkmQY+py/PYQGp4e7xgnHAeg87VeWb3A= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -468,6 +467,7 @@ github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= @@ -1359,8 +1359,9 @@ github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4 github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= @@ -2151,6 +2152,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -2279,6 +2281,7 @@ golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2465,6 +2468,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2474,6 +2478,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2811,6 +2816,7 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= @@ -2990,6 +2996,7 @@ k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= +k8s.io/cri-api v0.25.0/go.mod h1:J1rAyQkSJ2Q6I+aBMOVgg2/cbbebso6FNa0UagiR0kc= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= From ea05be970aecd2836eb937f515a51b47202fe101 Mon Sep 17 00:00:00 2001 From: Weston Steimel Date: Tue, 13 Dec 2022 15:08:46 +0000 Subject: [PATCH 5/5] chore: add GitLab Community Edition image to quality gate (#1035) Signed-off-by: Weston Steimel --- test/quality/.yardstick.yaml | 3 ++- test/quality/vulnerability-match-labels | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/quality/.yardstick.yaml b/test/quality/.yardstick.yaml index b52b7c5d2b8..0f3addeedbe 100644 --- a/test/quality/.yardstick.yaml +++ b/test/quality/.yardstick.yaml @@ -33,7 +33,8 @@ x-ref: - docker.io/jenkins/jenkins:2.361.4-lts-jdk11@sha256:6fd5699ab182b5d23d0e3936de6047edc30955a3a92e01c392d5a2fd583efac0 - docker.io/neo4j:4.4.14-community:4.4.14-community@sha256:fcfcbb026e0e538bf66f5fe5c4b2db3dd4931c3aae07f13a5a8c10e979596256 - docker.io/sonatype/nexus3:3.30.0@sha256:e8fea6b4279f2b5b24b36170459cb7aa3d6afe999f9d3e3713541be28bae8ec4 - + - docker.io/gitlab/gitlab-ce:15.6.1-ce.0@sha256:04d4219d5dfb3acccc9997e50477c8d24b371387a95857e1ea8fc779e17a716c + # new vulnerabilities are added all of the time, instead of keeping up it's easier to ignore newer entries. # This approach helps tremendously with keeping the analysis relatively stable. default_max_year: 2021 diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index c2876a52c5f..6120c083b3e 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit c2876a52c5f876bdf05439b46541f8a7a67b1f7c +Subproject commit 6120c083b3ea0544c2b8210018b992aaac337997