diff --git a/.gitignore b/.gitignore index d75b1549039..c395a26f3f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.tool-versions /go.work /go.work.sum /.grype.yaml diff --git a/README.md b/README.md index 7f3b11b3761..a71f5825361 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http - PHP (Composer) - Rust (Cargo) - Supports Docker, OCI and [Singularity](https://github.com/sylabs/singularity) image formats. +- [OpenVEX](https://github.com/openvex) support for filtering and augmenting scanning results. If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/grype/issues). @@ -322,6 +323,9 @@ ignore: # This is the full set of supported rule fields: - vulnerability: CVE-2008-4318 fix-state: unknown + # VEX fields apply when Grype reads vex data: + vex-status: not_affected + vex-justification: vulnerable_code_not_present package: name: libcurl version: 1.5.1 @@ -370,6 +374,78 @@ apk-tools 2.10.6-r0 2.10.7-r0 CVE-2021-36159 Critical If you want Grype to only report vulnerabilities **that do not have a confirmed fix**, you can use the `--only-notfixed` flag. (This automatically adds [ignore rules](#specifying-matches-to-ignore) into Grype's configuration, such that vulnerabilities that are fixed will be ignored.) +## VEX Support + +Grype can use VEX (Vulnerability Exploitability Exchange) data to filter false +positives or provide additional context, augmenting matches. When scanning a +container image, you can use the `--vex` flag to point to one or more +[OpenVEX](https://github.com/openvex) documents. + +VEX statements relate a product (a container image), a vulnerability, and a VEX +status to express an assertion of the vulnerability's impact. There are four +[VEX statuses](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-labels): +`not_affected`, `affected`, `fixed` and `under_investigation`. + +Here is an example of a simple OpenVEX document. (tip: use +[`vexctl`](https://github.com/openvex/vexctl) to generate your own documents). + +```json +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "A Grype User ", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-1255" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + } + ] +} +``` + +By default, Grype will use any statements in specified VEX documents with a +status of `not_affected` or `fixed` to move matches to the ignore set. + +Any matches ignored as a result of VEX statements are flagged when using +`--show-suppreessed`: + +``` +libcrypto3 3.0.8-r3 3.0.8-r4 apk CVE-2023-1255 Medium (suppressed by VEX) +``` + +Statements with an `affected` or `under_investigation` status will only be +considered to augment the result set when specifically requested using the +`GRYPE_VEX_ADD` environment variable or in a configuration file. + + +### VEX Ignore Rules + +Ignore rules can be written to control how Grype honors VEX statements. For +example, to configure Grype to only act on VEX statements when the justification is `vulnerable_code_not_present`, you can write a rule like this: + +```yaml +--- +ignore: + - vex-status: not_affected + vex-justification: vulnerable_code_not_present +``` + +See the [list of justifications](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications) for details. You can mix `vex-status` and `vex-justification` +with other ignore rule parameters. + ## Grype's database When Grype performs a scan for vulnerabilities, it does so using a vulnerability database that's stored on your local filesystem, which is constructed by pulling data from a variety of publicly available vulnerability data sources. These sources include: diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 5b6195c3b6c..e3dfc167a7c 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -30,6 +30,7 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/store" + "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/format" @@ -94,6 +95,11 @@ var ignoreFixedMatches = []match.IgnoreRule{ {FixState: string(grypeDb.FixedState)}, } +var ignoreVEXFixedNotAffected = []match.IgnoreRule{ + {VexStatus: string(vex.StatusNotAffected)}, + {VexStatus: string(vex.StatusFixed)}, +} + //nolint:funlen func runGrype(app clio.Application, opts *options.Grype, userInput string) error { errs := make(chan error) @@ -166,6 +172,11 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) error opts.Ignore = append(opts.Ignore, ignoreFixedMatches...) } + if err := applyVexRules(opts); err != nil { + errs <- fmt.Errorf("applying vex rules: %w", err) + return + } + applyDistroHint(packages, &pkgContext, opts) vulnMatcher := grype.VulnerabilityMatcher{ @@ -174,6 +185,10 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) error NormalizeByCVE: opts.ByCVE, FailSeverity: opts.FailOnServerity(), Matchers: getMatchers(opts), + VexProcessor: vex.NewProcessor(vex.ProcessorOptions{ + Documents: opts.VexDocuments, + IgnoreRules: opts.Ignore, + }), } remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext) @@ -342,3 +357,26 @@ func validateRootArgs(cmd *cobra.Command, args []string) error { return cobra.MaximumNArgs(1)(cmd, args) } + +func applyVexRules(opts *options.Grype) error { + if len(opts.Ignore) == 0 && len(opts.VexDocuments) > 0 { + opts.Ignore = append(opts.Ignore, ignoreVEXFixedNotAffected...) + } + + for _, vexStatus := range opts.VexAdd { + switch vexStatus { + case string(vex.StatusAffected): + opts.Ignore = append( + opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusAffected)}, + ) + case string(vex.StatusUnderInvestigation): + opts.Ignore = append( + opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusUnderInvestigation)}, + ) + default: + return fmt.Errorf("invalid VEX status in vex-add setting: %s", vexStatus) + } + } + + return nil +} diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go index 165f296cacc..d58d6be55ec 100644 --- a/cmd/grype/cli/options/grype.go +++ b/cmd/grype/cli/options/grype.go @@ -32,6 +32,8 @@ type Grype struct { 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 Name string `yaml:"name" json:"name" mapstructure:"name"` DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` + VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"` + VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD } var _ interface { @@ -46,9 +48,11 @@ func DefaultGrype(id clio.Identification) *Grype { Match: defaultMatchConfig(), ExternalSources: defaultExternalSources(), CheckForAppUpdate: true, + VexAdd: []string{}, } } +// nolint:funlen func (o *Grype) AddFlags(flags clio.FlagSet) { flags.StringVarP(&o.Search.Scope, "scope", "s", @@ -118,6 +122,11 @@ func (o *Grype) AddFlags(flags clio.FlagSet) { "platform", "", "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", ) + + flags.StringArrayVarP(&o.VexDocuments, + "vex", "", + "a list of VEX documents to consider when producing scanning results", + ) } func (o *Grype) PostLoad() error { diff --git a/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap index 6a0244c4192..2d424d73be9 100755 --- a/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap +++ b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap @@ -1,18 +1,18 @@ [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1] - ⠋ Scanning for vulnerabilities [20 vulnerabilities] + ⠋ Scanning for vulnerabilities [36 vulnerability matches] --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1] - ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) - └── 30 fixed + ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) + └── by status: 30 fixed, 10 not-fixed, 4 ignored (2 dropped) --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/task_line - 1] - ✔ Scanned for vulnerabilities [25 vulnerabilities] + ✔ Scanned for vulnerabilities [40 vulnerability matches] --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1] - ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) - └── 35 fixed + ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) + └── by status: 35 fixed, 10 not-fixed, 5 ignored (3 dropped) --- diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go index 54ef042bcb8..90a157df787 100644 --- a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go @@ -32,6 +32,9 @@ type vulnerabilityProgressTree struct { countBySeverity map[vulnerability.Severity]int64 unknownCount int64 fixedCount int64 + ignoredCount int64 + droppedCount int64 + totalCount int64 severities []vulnerability.Severity id uint32 @@ -65,19 +68,19 @@ type vulnerabilityScanningAdapter struct { } func (p vulnerabilityScanningAdapter) Current() int64 { - return p.mon.VulnerabilitiesDiscovered.Current() + return p.mon.PackagesProcessed.Current() } func (p vulnerabilityScanningAdapter) Error() error { - return p.mon.VulnerabilitiesDiscovered.Error() + return p.mon.MatchesDiscovered.Error() } func (p vulnerabilityScanningAdapter) Size() int64 { - return -1 + return p.mon.PackagesProcessed.Size() } func (p vulnerabilityScanningAdapter) Stage() string { - return fmt.Sprintf("%d vulnerabilities", p.mon.VulnerabilitiesDiscovered.Current()) + return fmt.Sprintf("%d vulnerability matches", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current()) } func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) []tea.Model { @@ -131,7 +134,10 @@ func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case vulnerabilityProgressTreeTickMsg: // update the model + l.totalCount = l.mon.MatchesDiscovered.Current() l.fixedCount = l.mon.Fixed.Current() + l.ignoredCount = l.mon.Ignored.Current() + l.droppedCount = l.mon.Dropped.Current() l.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current() for _, sev := range l.severities { l.countBySeverity[sev] = l.mon.BySeverity[sev].Current() @@ -164,12 +170,21 @@ func (l vulnerabilityProgressTree) View() string { status := sb.String() sb.Reset() - sevStr := l.textStyle.Render(fmt.Sprintf(" %s %s", branch, status)) - fixedStr := l.textStyle.Render(fmt.Sprintf(" %s %d fixed", end, l.fixedCount)) + sevStr := l.textStyle.Render(fmt.Sprintf(" %s by severity: %s", branch, status)) sb.WriteString(sevStr) - sb.WriteString("\n") - sb.WriteString(fixedStr) + + dropped := "" + if l.droppedCount > 0 { + dropped = fmt.Sprintf("(%d dropped)", l.droppedCount) + } + + fixedStr := l.textStyle.Render( + fmt.Sprintf(" %s by status: %d fixed, %d not-fixed, %d ignored %s", + end, l.fixedCount, l.totalCount-l.fixedCount, l.ignoredCount, dropped, + ), + ) + sb.WriteString("\n" + fixedStr) return sb.String() } diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go index ab23fd7d4fd..c3ce11b8fda 100644 --- a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go @@ -96,10 +96,10 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching { vulns := &progress.Manual{} vulns.SetTotal(-1) if completed { - vulns.Set(25) + vulns.Set(45) vulns.SetCompleted() } else { - vulns.Set(20) + vulns.Set(40) } fixed := &progress.Manual{} @@ -111,6 +111,24 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching { fixed.Set(30) } + ignored := &progress.Manual{} + ignored.SetTotal(-1) + if completed { + ignored.Set(5) + ignored.SetCompleted() + } else { + ignored.Set(4) + } + + dropped := &progress.Manual{} + dropped.SetTotal(-1) + if completed { + dropped.Set(3) + dropped.SetCompleted() + } else { + dropped.Set(2) + } + bySeverityWriter := map[vulnerability.Severity]*progress.Manual{ vulnerability.CriticalSeverity: {}, vulnerability.HighSeverity: {}, @@ -137,9 +155,11 @@ func getVulnerabilityMonitor(completed bool) monitor.Matching { } return monitor.Matching{ - PackagesProcessed: pkgs, - VulnerabilitiesDiscovered: vulns, - Fixed: fixed, - BySeverity: bySeverity, + PackagesProcessed: pkgs, + MatchesDiscovered: vulns, + Fixed: fixed, + Ignored: ignored, + Dropped: dropped, + BySeverity: bySeverity, } } diff --git a/go.mod b/go.mod index 4cd8037f783..5f097147b63 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/gkampitakis/go-snaps v0.4.10 github.com/go-test/deep v1.1.0 github.com/google/go-cmp v0.5.9 + github.com/google/go-containerregistry v0.16.1 github.com/google/uuid v1.3.1 github.com/gookit/color v1.5.4 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b @@ -40,6 +41,7 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 + github.com/openvex/go-vex v0.2.5 github.com/owenrumney/go-sarif v1.1.1 github.com/pkg/profile v1.7.0 // indirect // pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5 @@ -57,21 +59,20 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 golang.org/x/term v0.12.0 // indirect gorm.io/gorm v1.23.10 + modernc.org/sqlite v1.25.0 ) -require modernc.org/sqlite v1.25.0 - require ( - cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.3 // indirect + cloud.google.com/go v0.110.2 // indirect + cloud.google.com/go/compute v1.20.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/storage v1.28.1 // indirect + cloud.google.com/go/iam v1.1.0 // indirect + cloud.google.com/go/storage v1.29.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/DataDog/zstd v1.4.5 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect @@ -82,7 +83,7 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 // indirect github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect - github.com/aws/aws-sdk-go v1.44.180 // indirect + github.com/aws/aws-sdk-go v1.44.288 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect @@ -116,12 +117,11 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-containerregistry v0.16.1 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect - github.com/google/s2a-go v0.1.3 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.8.0 // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect + github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -154,15 +154,17 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/term v0.5.0 // indirect github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/nwaples/rardecode v1.1.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.19.0 // indirect + github.com/onsi/gomega v1.27.4 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/package-url/packageurl-go v0.1.1 // indirect github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect @@ -211,11 +213,13 @@ require ( golang.org/x/time v0.2.0 // indirect golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.122.0 // indirect + google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.55.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/grpc v1.56.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f5af9ed1473..e879b88a9fe 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -71,8 +71,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= @@ -113,14 +113,12 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= @@ -178,8 +176,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -209,8 +207,9 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -275,8 +274,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= -github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q= +github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= @@ -525,8 +524,8 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE= -github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -535,8 +534,8 @@ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -546,8 +545,8 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= @@ -734,8 +733,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -766,14 +765,18 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ= +github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo= github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE= github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= +github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= +github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= @@ -1391,8 +1394,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= -google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= +google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1508,8 +1511,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1546,8 +1553,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= +google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1564,8 +1571,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/grype/deprecated.go b/grype/deprecated.go index f67e1206355..5407d968665 100644 --- a/grype/deprecated.go +++ b/grype/deprecated.go @@ -5,13 +5,14 @@ import ( "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" + "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" ) -// TODO: deprecated, remove in v1.0.0 +// TODO: deprecated, will remove before 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{ @@ -31,7 +32,20 @@ func FindVulnerabilities(store store.Store, userImageStr string, scopeOpt source return FindVulnerabilitiesForPackage(store, context.Distro, matchers, packages), context, packages, nil } -// TODO: deprecated, remove in v1.0.0 +// TODO: deprecated, will remove before 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) + runner := VulnerabilityMatcher{ + Store: store, + Matchers: matchers, + NormalizeByCVE: false, + } + + actualResults, _, err := runner.FindMatches(packages, pkg.Context{ + Distro: d, + }) + if err != nil || actualResults == nil { + log.WithFields("error", err).Error("unable to find vulnerabilities") + return match.NewMatches() + } + return *actualResults } diff --git a/grype/event/monitor/matching.go b/grype/event/monitor/matching.go index 28967521174..f8280b09e36 100644 --- a/grype/event/monitor/matching.go +++ b/grype/event/monitor/matching.go @@ -7,8 +7,10 @@ import ( ) type Matching struct { - PackagesProcessed progress.Monitorable - VulnerabilitiesDiscovered progress.Monitorable - Fixed progress.Monitorable - BySeverity map[vulnerability.Severity]progress.Monitorable + PackagesProcessed progress.Progressable + MatchesDiscovered progress.Monitorable + Fixed progress.Monitorable + Ignored progress.Monitorable + Dropped progress.Monitorable + BySeverity map[vulnerability.Severity]progress.Monitorable } diff --git a/grype/match/ignore.go b/grype/match/ignore.go index cd719a70392..5984938d83d 100644 --- a/grype/match/ignore.go +++ b/grype/match/ignore.go @@ -17,10 +17,12 @@ type IgnoredMatch struct { // specified criteria must be met by the vulnerability match in order for the // rule to apply. type IgnoreRule struct { - Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"` - Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"` - FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"` - Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"` + Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"` + Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"` + FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"` + Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"` + VexStatus string `yaml:"vex-status" json:"vex-status" mapstructure:"vex-status"` + VexJustification string `yaml:"vex-justification" json:"vex-justification" mapstructure:"vex-justification"` } // IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule. @@ -67,6 +69,11 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa } func shouldIgnore(match Match, rule IgnoreRule) bool { + // VEX rules are handled by the vex processor + if rule.VexStatus != "" { + return false + } + ignoreConditions := getIgnoreConditionsForRule(rule) if len(ignoreConditions) == 0 { // this rule specifies no criteria, so it doesn't apply to the Match @@ -84,6 +91,12 @@ func shouldIgnore(match Match, rule IgnoreRule) bool { return true } +// HasConditions returns true if the ignore rule has conditions +// that can cause a match to be ignored +func (ir IgnoreRule) HasConditions() bool { + return len(getIgnoreConditionsForRule(ir)) == 0 +} + // An ignoreCondition is a function that returns a boolean indicating whether // the given Match should be ignored. type ignoreCondition func(match Match) bool diff --git a/grype/match/match.go b/grype/match/match.go index 128ceae169f..d7982335ef6 100644 --- a/grype/match/match.go +++ b/grype/match/match.go @@ -26,10 +26,6 @@ func (m Match) String() string { return fmt.Sprintf("Match(pkg=%s vuln=%q types=%q)", m.Package, m.Vulnerability.String(), m.Details.Types()) } -func (m Match) Summary() string { - return fmt.Sprintf("vuln=%q matchers=%s", m.Vulnerability.ID, m.Details.Matchers()) -} - func (m Match) Fingerprint() Fingerprint { return Fingerprint{ vulnerabilityID: m.Vulnerability.ID, diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index 6b596a88521..e8e8858f9cf 100644 --- a/grype/match/matcher_type.go +++ b/grype/match/matcher_type.go @@ -14,6 +14,7 @@ const ( MsrcMatcher MatcherType = "msrc-matcher" PortageMatcher MatcherType = "portage-matcher" GoModuleMatcher MatcherType = "go-module-matcher" + OpenVexMatcher MatcherType = "openvex-matcher" ) var AllMatcherTypes = []MatcherType{ @@ -28,6 +29,7 @@ var AllMatcherTypes = []MatcherType{ MsrcMatcher, PortageMatcher, GoModuleMatcher, + OpenVexMatcher, } type MatcherType string diff --git a/grype/match/matches.go b/grype/match/matches.go index 0df9a977884..e469c6dd3a1 100644 --- a/grype/match/matches.go +++ b/grype/match/matches.go @@ -52,6 +52,16 @@ func (r *Matches) Merge(other Matches) { } } +func (r *Matches) Diff(other Matches) *Matches { + diff := newMatches() + for fingerprint := range r.byFingerprint { + if _, exists := other.byFingerprint[fingerprint]; !exists { + diff.Add(r.byFingerprint[fingerprint]) + } + } + return &diff +} + func (r *Matches) Add(matches ...Match) { if len(matches) == 0 { return diff --git a/grype/match/matches_test.go b/grype/match/matches_test.go index 59d829335c4..b26e21c3c14 100644 --- a/grype/match/matches_test.go +++ b/grype/match/matches_test.go @@ -290,3 +290,67 @@ func assertIgnoredMatchOrder(t *testing.T, expected, actual []IgnoredMatch) { // make certain the fields are what you'd expect assert.Equal(t, expected, actual) } + +func TestMatches_Diff(t *testing.T) { + a := Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "vuln-a", + Namespace: "name-a", + }, + Package: pkg.Package{ + ID: "package-a", + }, + } + + b := Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "vuln-b", + Namespace: "name-b", + }, + Package: pkg.Package{ + ID: "package-b", + }, + } + + c := Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "vuln-c", + Namespace: "name-c", + }, + Package: pkg.Package{ + ID: "package-c", + }, + } + + tests := []struct { + name string + subject Matches + other Matches + want Matches + }{ + { + name: "no diff", + subject: NewMatches(a, b, c), + other: NewMatches(a, b, c), + want: newMatches(), + }, + { + name: "extra items in subject", + subject: NewMatches(a, b, c), + other: NewMatches(a, b), + want: NewMatches(c), + }, + { + // this demonstrates that this is not meant to implement a symmetric diff + name: "extra items in other (results in no diff)", + subject: NewMatches(a, b), + other: NewMatches(a, b, c), + want: NewMatches(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, &tt.want, tt.subject.Diff(tt.other), "Diff(%v)", tt.other) + }) + } +} diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 7181ac330b8..a27faa442f0 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -1,14 +1,6 @@ package matcher import ( - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - - grypeDb "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/grype/event/monitor" - "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/apk" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/dpkg" @@ -21,57 +13,8 @@ import ( "github.com/anchore/grype/grype/matcher/rpm" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" - "github.com/anchore/syft/syft/linux" - syftPkg "github.com/anchore/syft/syft/pkg" ) -type monitorWriter struct { - PackagesProcessed *progress.Manual - VulnerabilitiesDiscovered *progress.Manual - Fixed *progress.Manual - BySeverity map[vulnerability.Severity]*progress.Manual -} - -func newMonitor() (monitorWriter, monitor.Matching) { - manualBySev := make(map[vulnerability.Severity]*progress.Manual) - for _, severity := range vulnerability.AllSeverities() { - manualBySev[severity] = progress.NewManual(-1) - } - manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1) - - m := monitorWriter{ - PackagesProcessed: progress.NewManual(-1), - VulnerabilitiesDiscovered: progress.NewManual(-1), - Fixed: progress.NewManual(-1), - BySeverity: manualBySev, - } - - monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable) - for sev, manual := range manualBySev { - monitorableBySev[sev] = manual - } - - return m, monitor.Matching{ - PackagesProcessed: m.PackagesProcessed, - VulnerabilitiesDiscovered: m.VulnerabilitiesDiscovered, - Fixed: m.Fixed, - BySeverity: monitorableBySev, - } -} - -func (m *monitorWriter) SetCompleted() { - m.PackagesProcessed.SetCompleted() - m.VulnerabilitiesDiscovered.SetCompleted() - m.Fixed.SetCompleted() - for _, v := range m.BySeverity { - v.SetCompleted() - } -} - // Config contains values used by individual matcher structs for advanced configuration type Config struct { Java java.MatcherConfig @@ -99,165 +42,3 @@ func NewDefaultMatchers(mc Config) []Matcher { stock.NewStockMatcher(mc.Stock), } } - -func trackMatcher() *monitorWriter { - writer, reader := newMonitor() - - bus.Publish(partybus.Event{ - Type: event.VulnerabilityScanningStarted, - Value: reader, - }) - - return &writer -} - -func newMatcherIndex(matchers []Matcher) (map[syftPkg.Type][]Matcher, Matcher) { - matcherIndex := make(map[syftPkg.Type][]Matcher) - var defaultMatcher Matcher - for _, m := range matchers { - if m.Type() == match.StockMatcher { - defaultMatcher = m - continue - } - for _, t := range m.PackageTypes() { - if _, ok := matcherIndex[t]; !ok { - matcherIndex[t] = make([]Matcher, 0) - } - - matcherIndex[t] = append(matcherIndex[t], m) - log.Debugf("adding matcher: %+v", t) - } - } - - return matcherIndex, defaultMatcher -} - -func FindMatches(store interface { - vulnerability.Provider - vulnerability.MetadataProvider - match.ExclusionProvider -}, release *linux.Release, matchers []Matcher, packages []pkg.Package) match.Matches { - var err error - res := match.NewMatches() - matcherIndex, defaultMatcher := newMatcherIndex(matchers) - - var ignored []match.IgnoredMatch - - var d *distro.Distro - if release != nil { - d, err = distro.NewFromRelease(*release) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) - } - if d != nil && d.Disabled() { - log.Warnf("unsupported linux distribution: %s", d.Name()) - return match.Matches{} - } - } - - progressMonitor := trackMatcher() - - if defaultMatcher == nil { - defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) - } - for _, p := range packages { - progressMonitor.PackagesProcessed.Increment() - log.Debugf("searching for vulnerability matches for pkg=%s", p) - - matchAgainst, ok := matcherIndex[p.Type] - if !ok { - matchAgainst = []Matcher{defaultMatcher} - } - for _, m := range matchAgainst { - matches, err := m.Match(store, d, p) - if err != nil { - log.Warnf("matcher failed for pkg=%s: %+v", p, err) - } else { - // Filter out matches based on records in the database exclusion table and hard-coded rules - filtered, ignores := match.ApplyExplicitIgnoreRules(store, match.NewMatches(matches...)) - ignored = append(ignored, ignores...) - matches := filtered.Sorted() - logMatches(p, matches) - res.Add(matches...) - progressMonitor.VulnerabilitiesDiscovered.Add(int64(len(matches))) - updateVulnerabilityList(progressMonitor, matches, store) - } - } - } - - progressMonitor.SetCompleted() - - logListSummary(progressMonitor) - - logIgnoredMatches(ignored) - - return res -} - -func logListSummary(vl *monitorWriter) { - log.Infof("found %d vulnerabilities for %d packages", vl.VulnerabilitiesDiscovered.Current(), vl.PackagesProcessed.Current()) - log.Debugf(" ├── fixed: %d", vl.Fixed.Current()) - log.Debugf(" └── matched: %d", vl.VulnerabilitiesDiscovered.Current()) - - var unknownCount int64 - if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok { - unknownCount = count.Current() - } - log.Debugf(" ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount) - - allSeverities := vulnerability.AllSeverities() - for idx, sev := range allSeverities { - branch := "├" - if idx == len(allSeverities)-1 { - branch = "└" - } - log.Debugf(" %s── %s: %d", branch, sev.String(), vl.BySeverity[sev].Current()) - } -} - -func logIgnoredMatches(ignored []match.IgnoredMatch) { - if len(ignored) > 0 { - log.Debugf("Removed %d explicit vulnerability matches:", len(ignored)) - for idx, i := range ignored { - branch := "├──" - if idx == len(ignored)-1 { - branch = "└──" - } - log.Debugf(" %s %s : %s", branch, i.Match.Vulnerability.ID, i.Package.PURL) - } - } -} - -func updateVulnerabilityList(list *monitorWriter, matches []match.Match, metadataProvider vulnerability.MetadataProvider) { - for _, m := range matches { - metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) - if err != nil || metadata == nil { - list.BySeverity[vulnerability.UnknownSeverity].Increment() - continue - } - - sevManualProgress, ok := list.BySeverity[vulnerability.ParseSeverity(metadata.Severity)] - if !ok { - list.BySeverity[vulnerability.UnknownSeverity].Increment() - continue - } - sevManualProgress.Increment() - - if m.Vulnerability.Fix.State == grypeDb.FixedState { - list.Fixed.Increment() - } - } -} - -func logMatches(p pkg.Package, matches []match.Match) { - if len(matches) > 0 { - log.Debugf("found %d vulnerabilities for pkg=%s", len(matches), p) - for idx, m := range matches { - var branch = "├──" - if idx == len(matches)-1 { - branch = "└──" - } - log.Debugf(" %s %s", branch, m.Summary()) - } - } -} diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go index c1471822b77..2ef882518e1 100644 --- a/grype/presenter/internal/test_helpers.go +++ b/grype/presenter/internal/test_helpers.go @@ -10,6 +10,7 @@ import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft/artifact" @@ -148,64 +149,89 @@ func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches { return collection } +// nolint: funlen func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { t.Helper() - matches := []match.Match{ + return []match.IgnoredMatch{ { - - Vulnerability: vulnerability.Vulnerability{ - ID: "CVE-1999-0001", - Namespace: "source-1", - }, - Package: p, - Details: []match.Detail{ - { - Type: match.ExactDirectMatch, - Matcher: match.DpkgMatcher, - SearchedBy: map[string]interface{}{ - "distro": map[string]string{ - "type": "ubuntu", - "version": "20.04", + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0001", + Namespace: "source-1", + }, + Package: p, + Details: []match.Detail{ + { + Type: match.ExactDirectMatch, + Matcher: match.DpkgMatcher, + SearchedBy: map[string]interface{}{ + "distro": map[string]string{ + "type": "ubuntu", + "version": "20.04", + }, + }, + Found: map[string]interface{}{ + "constraint": ">= 20", }, }, - Found: map[string]interface{}{ - "constraint": ">= 20", + }, + }, + AppliedIgnoreRules: []match.IgnoreRule{}, + }, + { + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0002", + Namespace: "source-2", + }, + Package: p, + Details: []match.Detail{ + { + Type: match.ExactDirectMatch, + Matcher: match.DpkgMatcher, + SearchedBy: map[string]interface{}{ + "cpe": "somecpe", + }, + Found: map[string]interface{}{ + "constraint": "somecpe", + }, }, }, }, + AppliedIgnoreRules: []match.IgnoreRule{}, }, { - - Vulnerability: vulnerability.Vulnerability{ - ID: "CVE-1999-0002", - Namespace: "source-2", + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-1999-0004", + Namespace: "source-2", + }, + Package: p, + Details: []match.Detail{ + { + Type: match.ExactDirectMatch, + Matcher: match.DpkgMatcher, + SearchedBy: map[string]interface{}{ + "cpe": "somecpe", + }, + Found: map[string]interface{}{ + "constraint": "somecpe", + }, + }, + }, }, - Package: p, - Details: []match.Detail{ + AppliedIgnoreRules: []match.IgnoreRule{ { - Type: match.ExactDirectMatch, - Matcher: match.DpkgMatcher, - SearchedBy: map[string]interface{}{ - "cpe": "somecpe", - }, - Found: map[string]interface{}{ - "constraint": "somecpe", - }, + Vulnerability: "CVE-1999-0004", + Namespace: "vex", + Package: match.IgnoreRulePackage{}, + VexStatus: string(vex.StatusNotAffected), + VexJustification: "this isn't the vulnerability match you're looking for... *waves hand*", }, }, }, } - - var ignoredMatches []match.IgnoredMatch - for _, m := range matches { - ignoredMatches = append(ignoredMatches, match.IgnoredMatch{ - Match: m, - AppliedIgnoreRules: []match.IgnoreRule{}, - }) - } - - return ignoredMatches } func generatePackages(t *testing.T) []pkg.Package { diff --git a/grype/presenter/models/ignore.go b/grype/presenter/models/ignore.go index 4f4176945e4..b85465de8b3 100644 --- a/grype/presenter/models/ignore.go +++ b/grype/presenter/models/ignore.go @@ -8,9 +8,11 @@ type IgnoredMatch struct { } type IgnoreRule struct { - Vulnerability string `json:"vulnerability,omitempty"` - FixState string `json:"fix-state,omitempty"` - Package *IgnoreRulePackage `json:"package,omitempty"` + Vulnerability string `json:"vulnerability,omitempty"` + FixState string `json:"fix-state,omitempty"` + Package *IgnoreRulePackage `json:"package,omitempty"` + VexStatus string `json:"vex-status,omitempty"` + VexJustification string `json:"vex-justification,omitempty"` } type IgnoreRulePackage struct { @@ -34,9 +36,11 @@ func newIgnoreRule(r match.IgnoreRule) IgnoreRule { } return IgnoreRule{ - Vulnerability: r.Vulnerability, - FixState: r.FixState, - Package: ignoreRulePackage, + Vulnerability: r.Vulnerability, + FixState: r.FixState, + Package: ignoreRulePackage, + VexStatus: r.VexStatus, + VexJustification: r.VexJustification, } } diff --git a/grype/presenter/models/metadata_mock.go b/grype/presenter/models/metadata_mock.go index 326fec60f9e..cade2230f02 100644 --- a/grype/presenter/models/metadata_mock.go +++ b/grype/presenter/models/metadata_mock.go @@ -60,6 +60,27 @@ func NewMetadataMock() *MetadataMock { Severity: "High", }, }, + "CVE-1999-0004": { + "source-2": { + Description: "1999-04 description", + Severity: "Critical", + Cvss: []vulnerability.Cvss{ + { + Metrics: vulnerability.NewCvssMetrics( + 1, + 2, + 3, + ), + Vector: "vector", + Version: "2.0", + VendorMetadata: MockVendorMetadata{ + BaseSeverity: "Low", + Status: "verified", + }, + }, + }, + }, + }, }, } } diff --git a/grype/presenter/table/__snapshots__/presenter_test.snap b/grype/presenter/table/__snapshots__/presenter_test.snap new file mode 100755 index 00000000000..11c2b0a8ba9 --- /dev/null +++ b/grype/presenter/table/__snapshots__/presenter_test.snap @@ -0,0 +1,29 @@ + +[TestTablePresenter - 1] +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY +package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low +package-2 2.2.2 deb CVE-1999-0002 Critical + +--- + +[TestEmptyTablePresenter - 1] +No vulnerabilities found + +--- + +[TestHidesIgnoredMatches - 1] +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY +package-1 1.1.1 rpm CVE-1999-0002 Critical +package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low + +--- + +[TestDisplaysIgnoredMatches - 1] +NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY +package-1 1.1.1 rpm CVE-1999-0002 Critical +package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low +package-2 2.2.2 deb CVE-1999-0004 Critical (suppressed by VEX) +package-2 2.2.2 deb CVE-1999-0002 Critical (suppressed) +package-2 2.2.2 deb CVE-1999-0001 Low (suppressed) + +--- diff --git a/grype/presenter/table/presenter.go b/grype/presenter/table/presenter.go index 4d7f5058fe4..4c0a4d3cfea 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -16,7 +16,8 @@ import ( ) const ( - appendSuppressed = " (suppressed)" + appendSuppressed = " (suppressed)" + appendSuppressedVEX = " (suppressed by VEX)" ) // Presenter is a generic struct for holding fields needed for reporting @@ -56,7 +57,15 @@ func (pres *Presenter) Present(output io.Writer) error { // Generate rows for suppressed vulnerabilities if pres.showSuppressed { for _, m := range pres.ignoredMatches { - row, err := createRow(m.Match, pres.metadataProvider, appendSuppressed) + msg := appendSuppressed + if m.AppliedIgnoreRules != nil { + for i := range m.AppliedIgnoreRules { + if m.AppliedIgnoreRules[i].Namespace == "vex" { + msg = appendSuppressedVEX + } + } + } + row, err := createRow(m.Match, pres.metadataProvider, msg) if err != nil { return err diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index ee45e0769d9..c2c7e789893 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -2,15 +2,14 @@ package table import ( "bytes" - "flag" "testing" + "github.com/gkampitakis/go-snaps/snaps" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" - "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/internal" @@ -19,8 +18,6 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) -var update = flag.Bool("update", false, "update the *.golden files for table presenters") - func TestCreateRow(t *testing.T) { pkg1 := pkg.Package{ ID: "package-1-id", @@ -88,21 +85,10 @@ func TestTablePresenter(t *testing.T) { // run presenter err := pres.Present(&buffer) - if err != nil { - t.Fatal(err) - } - actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } + require.NoError(t, err) - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } + actual := buffer.String() + snaps.MatchSnapshot(t, actual) // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) @@ -125,22 +111,10 @@ func TestEmptyTablePresenter(t *testing.T) { // run presenter err := pres.Present(&buffer) - if err != nil { - t.Fatal(err) - } - actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } - - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } + require.NoError(t, err) + actual := buffer.String() + snaps.MatchSnapshot(t, actual) } func TestRemoveDuplicateRows(t *testing.T) { @@ -215,21 +189,10 @@ func TestHidesIgnoredMatches(t *testing.T) { pres := NewPresenter(pb, false) err := pres.Present(&buffer) - if err != nil { - t.Fatal(err) - } - actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } + require.NoError(t, err) - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } + actual := buffer.String() + snaps.MatchSnapshot(t, actual) } func TestDisplaysIgnoredMatches(t *testing.T) { @@ -246,19 +209,8 @@ func TestDisplaysIgnoredMatches(t *testing.T) { pres := NewPresenter(pb, true) err := pres.Present(&buffer) - if err != nil { - t.Fatal(err) - } - actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } + require.NoError(t, err) - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } + actual := buffer.String() + snaps.MatchSnapshot(t, actual) } diff --git a/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden b/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden deleted file mode 100644 index 3c512570384..00000000000 --- a/grype/presenter/table/test-fixtures/snapshot/TestDisplaysIgnoredMatches.golden +++ /dev/null @@ -1,5 +0,0 @@ -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 rpm CVE-1999-0002 Critical -package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical (suppressed) -package-2 2.2.2 deb CVE-1999-0001 Low (suppressed) diff --git a/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden b/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden deleted file mode 100644 index 8900c02cd74..00000000000 --- a/grype/presenter/table/test-fixtures/snapshot/TestEmptyTablePresenter.golden +++ /dev/null @@ -1 +0,0 @@ -No vulnerabilities found diff --git a/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden b/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden deleted file mode 100644 index 4d13482c154..00000000000 --- a/grype/presenter/table/test-fixtures/snapshot/TestHidesIgnoredMatches.golden +++ /dev/null @@ -1,3 +0,0 @@ -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 rpm CVE-1999-0002 Critical -package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low diff --git a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden deleted file mode 100644 index e16b5919029..00000000000 --- a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden +++ /dev/null @@ -1,3 +0,0 @@ -NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY -package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low -package-2 2.2.2 deb CVE-1999-0002 Critical diff --git a/grype/vex/openvex/implementation.go b/grype/vex/openvex/implementation.go new file mode 100644 index 00000000000..fa20ce75222 --- /dev/null +++ b/grype/vex/openvex/implementation.go @@ -0,0 +1,324 @@ +package openvex + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + openvex "github.com/openvex/go-vex/pkg/vex" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/source" +) + +type Processor struct{} + +func New() *Processor { + return &Processor{} +} + +// Match captures the criteria that caused a vulnerability to match +type Match struct { + Statement openvex.Statement +} + +// SearchedBy captures the prameters used to search through the VEX data +type SearchedBy struct { + Vulnerability string + Product string + Subcomponents []string +} + +// augmentStatuses are the VEX statuses that augment results +var augmentStatuses = []openvex.Status{ + openvex.StatusAffected, + openvex.StatusUnderInvestigation, +} + +// filterStatuses are the VEX statuses that filter matched to the ignore list +var ignoreStatuses = []openvex.Status{ + openvex.StatusNotAffected, + openvex.StatusFixed, +} + +// ReadVexDocuments reads and merges VEX documents +func (ovm *Processor) ReadVexDocuments(docs []string) (interface{}, error) { + // Combine all VEX documents into a single VEX document + vexdata, err := openvex.MergeFiles(docs) + if err != nil { + return nil, fmt.Errorf("merging vex documents: %w", err) + } + + return vexdata, nil +} + +// productIdentifiersFromContext reads the package context and returns software +// identifiers identifying the scanned image. +func productIdentifiersFromContext(pkgContext *pkg.Context) ([]string, error) { + switch v := pkgContext.Source.Metadata.(type) { + case source.StereoscopeImageSourceMetadata: + // TODO(puerco): We can create a wider definition here. This effectively + // adds the multiarch image and the image of the OS running grype. We + // could generate more identifiers to match better. + return identifiersFromDigests(v.RepoDigests), nil + default: + // Fail for now + return nil, errors.New("source type not supported for VEX") + } +} + +func identifiersFromDigests(digests []string) []string { + identifiers := []string{} + + for _, d := range digests { + // The first identifier is the original image reference: + identifiers = append(identifiers, d) + + // Not an image reference, skip + ref, err := name.ParseReference(d) + if err != nil { + continue + } + + var digestString, repoURL string + shaString := ref.Identifier() + + // If not a digest, we can't form a purl, so skip it + if !strings.HasPrefix(shaString, "sha256:") { + continue + } + + digestString = url.QueryEscape(shaString) + + pts := strings.Split(ref.Context().RepositoryStr(), "/") + name := pts[len(pts)-1] + repoURL = strings.TrimSuffix( + ref.Context().RegistryStr()+"/"+ref.Context().RepositoryStr(), + fmt.Sprintf("/%s", name), + ) + + qMap := map[string]string{} + + if repoURL != "" { + qMap["repository_url"] = repoURL + } + qs := packageurl.QualifiersFromMap(qMap) + identifiers = append(identifiers, packageurl.NewPackageURL( + "oci", "", name, digestString, qs, "", + ).String()) + + // Add a hash to the identifier list in case people want to vex + // using the value of the image digest + identifiers = append(identifiers, strings.TrimPrefix(shaString, "sha256:")) + } + return identifiers +} + +// subcomponentIdentifiersFromMatch returns the list of identifiers from the +// package where grype did the match. +func subcomponentIdentifiersFromMatch(m *match.Match) []string { + ret := []string{} + if m.Package.PURL != "" { + ret = append(ret, m.Package.PURL) + } + + // TODO(puerco):Implement CPE matching in openvex/go-vex + /* + for _, c := range m.Package.CPEs { + ret = append(ret, c.String()) + } + */ + return ret +} + +// FilterMatches takes a set of scanning results and moves any results marked in +// the VEX data as fixed or not_affected to the ignored list. +func (ovm *Processor) FilterMatches( + docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch, +) (*match.Matches, []match.IgnoredMatch, error) { + doc, ok := docRaw.(*openvex.VEX) + if !ok { + return nil, nil, errors.New("unable to cast vex document as openvex") + } + + remainingMatches := match.NewMatches() + + products, err := productIdentifiersFromContext(pkgContext) + if err != nil { + return nil, nil, fmt.Errorf("reading product identifiers from context: %w", err) + } + + // TODO(alex): should we apply the vex ignore rules to the already ignored matches? + // that way the end user sees all of the reasons a match was ignored in case multiple apply + + // Now, let's go through grype's matches + sorted := matches.Sorted() + for i := range sorted { + var statement *openvex.Statement + subcmp := subcomponentIdentifiersFromMatch(&sorted[i]) + + // Range through the product's different names + for _, product := range products { + if matchingStatements := doc.Matches(sorted[i].Vulnerability.ID, product, subcmp); len(matchingStatements) != 0 { + statement = &matchingStatements[0] + break + } + } + + // No data about this match's component. Next. + if statement == nil { + remainingMatches.Add(sorted[i]) + continue + } + + rule := matchingRule(ignoreRules, sorted[i], statement, ignoreStatuses) + if rule == nil { + remainingMatches.Add(sorted[i]) + continue + } + + // Filtering only applies to not_affected and fixed statuses + if statement.Status != openvex.StatusNotAffected && statement.Status != openvex.StatusFixed { + remainingMatches.Add(sorted[i]) + continue + } + + ignoredMatches = append(ignoredMatches, match.IgnoredMatch{ + Match: sorted[i], + AppliedIgnoreRules: []match.IgnoreRule{*rule}, + }) + } + return &remainingMatches, ignoredMatches, nil +} + +// matchingRule cycles through a set of ignore rules and returns the first +// one that matches the statement and the match. Returns nil if none match. +func matchingRule(ignoreRules []match.IgnoreRule, m match.Match, statement *openvex.Statement, allowedStatuses []openvex.Status) *match.IgnoreRule { + ms := match.NewMatches() + ms.Add(m) + + revStatuses := map[string]struct{}{} + for _, s := range allowedStatuses { + revStatuses[string(s)] = struct{}{} + } + + for _, rule := range ignoreRules { + // If the rule has more conditions than just the VEX statement, check if + // it applies to the current match. + if rule.HasConditions() { + r := rule + r.VexStatus = "" + if _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 { + continue + } + } + + // If the status in the statement is not the same in the rule + // and the vex statement, it does not apply + if string(statement.Status) != rule.VexStatus { + continue + } + + // If the rule has a statement other than the allowed ones, skip: + if len(revStatuses) > 0 && rule.VexStatus != "" { + if _, ok := revStatuses[rule.VexStatus]; !ok { + continue + } + } + + // If the rule applies to a VEX justification it needs to match the + // statement, note that justifications only apply to not_affected: + if statement.Status == openvex.StatusNotAffected && rule.VexJustification != "" && + rule.VexJustification != string(statement.Justification) { + continue + } + + // If the vulnerability is blank in the rule it means we will honor + // any status with any vulnerability. + if rule.Vulnerability == "" { + return &rule + } + + // If the vulnerability is set, the rule applies if it is the same + // in the statement and the rule. + if statement.Vulnerability.Matches(rule.Vulnerability) { + return &rule + } + } + return nil +} + +// AugmentMatches adds results to the match.Matches array when matching data +// about an affected VEX product is found on loaded VEX documents. Matches +// are moved from the ignore list or synthesized when no previous data is found. +func (ovm *Processor) AugmentMatches( + docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, +) (*match.Matches, []match.IgnoredMatch, error) { + doc, ok := docRaw.(*openvex.VEX) + if !ok { + return nil, nil, errors.New("unable to cast vex document as openvex") + } + + additionalIgnoredMatches := []match.IgnoredMatch{} + + products, err := productIdentifiersFromContext(pkgContext) + if err != nil { + return nil, nil, fmt.Errorf("reading product identifiers from context: %w", err) + } + + // Now, let's go through grype's matches + for i := range ignoredMatches { + var statement *openvex.Statement + var searchedBy *SearchedBy + subcmp := subcomponentIdentifiersFromMatch(&ignoredMatches[i].Match) + + // Range through the product's different names to see if they match the + // statement data + for _, product := range products { + if matchingStatements := doc.Matches(ignoredMatches[i].Vulnerability.ID, product, subcmp); len(matchingStatements) != 0 { + if matchingStatements[0].Status != openvex.StatusAffected && + matchingStatements[0].Status != openvex.StatusUnderInvestigation { + break + } + statement = &matchingStatements[0] + searchedBy = &SearchedBy{ + Vulnerability: ignoredMatches[i].Vulnerability.ID, + Product: product, + Subcomponents: subcmp, + } + break + } + } + + // No data about this match's component. Next. + if statement == nil { + additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i]) + continue + } + + // Only match if rules to augment are configured + rule := matchingRule(ignoreRules, ignoredMatches[i].Match, statement, augmentStatuses) + if rule == nil { + additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i]) + continue + } + + newMatch := ignoredMatches[i].Match + newMatch.Details = append(newMatch.Details, match.Detail{ + Type: match.ExactDirectMatch, + SearchedBy: searchedBy, + Found: Match{ + Statement: *statement, + }, + Matcher: match.OpenVexMatcher, + }) + + remainingMatches.Add(newMatch) + } + + return remainingMatches, additionalIgnoredMatches, nil +} diff --git a/grype/vex/openvex/implementation_test.go b/grype/vex/openvex/implementation_test.go new file mode 100644 index 00000000000..6407df46e24 --- /dev/null +++ b/grype/vex/openvex/implementation_test.go @@ -0,0 +1,38 @@ +package openvex + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIdentifiersFromDigests(t *testing.T) { + for _, tc := range []struct { + sut string + expected []string + }{ + { + "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + []string{ + "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io/library", + "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + }, + }, + { + "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", + []string{ + "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", + "pkg:oci/curl@sha256%3A9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc?repository_url=cgr.dev/chainguard", + "9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", + }, + }, + { + "alpine", + []string{"alpine"}, + }, + } { + res := identifiersFromDigests([]string{tc.sut}) + require.Equal(t, tc.expected, res) + } +} diff --git a/grype/vex/processor.go b/grype/vex/processor.go new file mode 100644 index 00000000000..2c744d9f360 --- /dev/null +++ b/grype/vex/processor.go @@ -0,0 +1,112 @@ +package vex + +import ( + "fmt" + + gopenvex "github.com/openvex/go-vex/pkg/vex" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vex/openvex" +) + +type Status string + +const ( + StatusNotAffected Status = Status(gopenvex.StatusNotAffected) + StatusAffected Status = Status(gopenvex.StatusAffected) + StatusFixed Status = Status(gopenvex.StatusFixed) + StatusUnderInvestigation Status = Status(gopenvex.StatusUnderInvestigation) +) + +type Processor struct { + Options ProcessorOptions + impl vexProcessorImplementation +} + +type vexProcessorImplementation interface { + // ReadVexDocuments takes a list of vex filenames and returns a single + // value representing the VEX information in the underlying implementation's + // format. Returns an error if the files cannot be processed. + ReadVexDocuments(docs []string) (interface{}, error) + + // FilterMatches matches receives the underlying VEX implementation VEX data and + // the scanning context and matching results and filters the fixed and + // not_affected results,moving them to the list of ignored matches. + FilterMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) + + // AugmentMatches reads known affected VEX products from loaded documents and + // adds new results to the scanner results when the product is marked as + // affected in the VEX data. + AugmentMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) +} + +// getVexImplementation this function returns the vex processor implementation +// at some point it can read the options and choose a user configured implementation. +func getVexImplementation() vexProcessorImplementation { + return openvex.New() +} + +// NewProcessor returns a new VEX processor. For now, it defaults to the only vex +// implementation: OpenVEX +func NewProcessor(opts ProcessorOptions) *Processor { + return &Processor{ + Options: opts, + impl: getVexImplementation(), + } +} + +// ProcessorOptions captures the optiones of the VEX processor. +type ProcessorOptions struct { + Documents []string + IgnoreRules []match.IgnoreRule +} + +// ApplyVEX receives the results from a scan run and applies any VEX information +// in the files specified in the grype invocation. Any filtered results will +// be moved to the ignored matches slice. +func (vm *Processor) ApplyVEX(pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) { + var err error + + // If no VEX documents are loaded, just pass through the matches, effectivle NOOP + if len(vm.Options.Documents) == 0 { + return remainingMatches, ignoredMatches, nil + } + + // Read VEX data from all passed documents + rawVexData, err := vm.impl.ReadVexDocuments(vm.Options.Documents) + if err != nil { + return nil, nil, fmt.Errorf("parsing vex document: %w", err) + } + + vexRules := extractVexRules(vm.Options.IgnoreRules) + + remainingMatches, ignoredMatches, err = vm.impl.FilterMatches( + rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches, + ) + if err != nil { + return nil, nil, fmt.Errorf("checking matches against VEX data: %w", err) + } + + remainingMatches, ignoredMatches, err = vm.impl.AugmentMatches( + rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches, + ) + if err != nil { + return nil, nil, fmt.Errorf("checking matches to augment from VEX data: %w", err) + } + + return remainingMatches, ignoredMatches, nil +} + +// extractVexRules is a utility function that takes a set of ignore rules and +// extracts those that act on VEX statuses. +func extractVexRules(rules []match.IgnoreRule) []match.IgnoreRule { + newRules := []match.IgnoreRule{} + for _, r := range rules { + if r.VexStatus != "" { + newRules = append(newRules, r) + newRules[len(newRules)-1].Namespace = "vex" + } + } + return newRules +} diff --git a/grype/vex/processor_test.go b/grype/vex/processor_test.go new file mode 100644 index 00000000000..85168f2b1ab --- /dev/null +++ b/grype/vex/processor_test.go @@ -0,0 +1,314 @@ +package vex + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v5 "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/syft/syft/source" +) + +func TestProcessor_ApplyVEX(t *testing.T) { + pkgContext := &pkg.Context{ + Source: &source.Description{ + Name: "alpine", + Version: "3.17", + Metadata: source.StereoscopeImageSourceMetadata{ + RepoDigests: []string{ + "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + }, + }, + }, + Distro: nil, + } + + libCryptoPackage := pkg.Package{ + ID: "cc8f90662d91481d", + Name: "libcrypto3", + Version: "3.0.8-r3", + + Type: "apk", + PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3", + Upstreams: []pkg.UpstreamPackage{ + { + Name: "openssl", + }, + }, + } + + libCryptoCVE_2023_3817 := match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-2023-3817", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.10-r0"}, + State: v5.FixedState, + }, + }, + Package: libCryptoPackage, + } + + libCryptoCVE_2023_1255 := match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-2023-1255", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.8-r4"}, + State: v5.FixedState, + }, + }, + Package: libCryptoPackage, + } + + libCryptoCVE_2023_2975 := match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-2023-2975", + Namespace: "alpine:distro:alpine:3.17", + Fix: vulnerability.Fix{ + Versions: []string{"3.0.9-r2"}, + State: v5.FixedState, + }, + }, + Package: libCryptoPackage, + } + + getSubject := func() *match.Matches { + s := match.NewMatches( + // not-affected justification example + libCryptoCVE_2023_3817, + + // fixed status example + matching CVE + libCryptoCVE_2023_1255, + + // fixed status example + libCryptoCVE_2023_2975, + ) + + return &s + } + + metchesRef := func(ms ...match.Match) *match.Matches { + m := match.NewMatches(ms...) + return &m + } + + type args struct { + pkgContext *pkg.Context + matches *match.Matches + ignoredMatches []match.IgnoredMatch + } + + tests := []struct { + name string + options ProcessorOptions + args args + wantMatches *match.Matches + wantIgnoredMatches []match.IgnoredMatch + wantErr require.ErrorAssertionFunc + }{ + { + name: "openvex-demo1 - ignore by fixed status", + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo1.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + VexStatus: "fixed", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + wantMatches: metchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), + wantIgnoredMatches: []match.IgnoredMatch{ + { + Match: libCryptoCVE_2023_1255, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", // note: an additional namespace was added + VexStatus: "fixed", + }, + }, + }, + }, + }, + { + name: "openvex-demo1 - ignore by fixed status and CVE", // no real difference from the first test other than the AppliedIgnoreRules + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo1.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test + VexStatus: "fixed", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + wantMatches: metchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), + wantIgnoredMatches: []match.IgnoredMatch{ + { + Match: libCryptoCVE_2023_1255, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", + Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test + VexStatus: "fixed", + }, + }, + }, + }, + }, + { + name: "openvex-demo2 - ignore by fixed status", + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo2.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + VexStatus: "fixed", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + wantMatches: metchesRef(libCryptoCVE_2023_3817), + wantIgnoredMatches: []match.IgnoredMatch{ + { + Match: libCryptoCVE_2023_1255, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", + VexStatus: "fixed", + }, + }, + }, + { + Match: libCryptoCVE_2023_2975, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", + VexStatus: "fixed", + }, + }, + }, + }, + }, + { + name: "openvex-demo2 - ignore by fixed status and CVE", + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo2.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test + VexStatus: "fixed", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + wantMatches: metchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), + wantIgnoredMatches: []match.IgnoredMatch{ + { + Match: libCryptoCVE_2023_1255, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", + Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test + VexStatus: "fixed", + }, + }, + }, + }, + }, + { + name: "openvex-demo1 - ignore by not_affected status and vulnerable_code_not_present justification", + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo1.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + VexStatus: "not_affected", + VexJustification: "vulnerable_code_not_present", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + // nothing gets ignored! + wantMatches: metchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975, libCryptoCVE_2023_1255), + wantIgnoredMatches: []match.IgnoredMatch{}, + }, + { + name: "openvex-demo2 - ignore by not_affected status and vulnerable_code_not_present justification", + options: ProcessorOptions{ + Documents: []string{ + "testdata/vex-docs/openvex-demo2.json", + }, + IgnoreRules: []match.IgnoreRule{ + { + VexStatus: "not_affected", + VexJustification: "vulnerable_code_not_present", + }, + }, + }, + args: args{ + pkgContext: pkgContext, + matches: getSubject(), + }, + wantMatches: metchesRef(libCryptoCVE_2023_2975, libCryptoCVE_2023_1255), + wantIgnoredMatches: []match.IgnoredMatch{ + { + Match: libCryptoCVE_2023_3817, + AppliedIgnoreRules: []match.IgnoreRule{ + { + Namespace: "vex", + VexStatus: "not_affected", + VexJustification: "vulnerable_code_not_present", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + p := NewProcessor(tt.options) + actualMatches, actualIgnoredMatches, err := p.ApplyVEX(tt.args.pkgContext, tt.args.matches, tt.args.ignoredMatches) + tt.wantErr(t, err) + if err != nil { + return + } + + assert.Equal(t, tt.wantMatches.Sorted(), actualMatches.Sorted()) + assert.Equal(t, tt.wantIgnoredMatches, actualIgnoredMatches) + + }) + } +} diff --git a/grype/vex/testdata/vex-docs/openvex-demo1.json b/grype/vex/testdata/vex-docs/openvex-demo1.json new file mode 100644 index 00000000000..47549499ad5 --- /dev/null +++ b/grype/vex/testdata/vex-docs/openvex-demo1.json @@ -0,0 +1,24 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "The OpenVEX Project ", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-1255" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + } + ] +} diff --git a/grype/vex/testdata/vex-docs/openvex-demo2.json b/grype/vex/testdata/vex-docs/openvex-demo2.json new file mode 100644 index 00000000000..637d0907822 --- /dev/null +++ b/grype/vex/testdata/vex-docs/openvex-demo2.json @@ -0,0 +1,89 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "The OpenVEX Project ", + "role": "Demo Writer", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-1255" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-2650" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-2975" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-3446" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "affected functions were removed before packaging" + }, + { + "vulnerability": { + "name": "CVE-2023-3817" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "affected functions were removed before packaging" + } + ] +} diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index 3eff7f81d25..e26dc323115 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -1,15 +1,33 @@ package grype import ( + "fmt" "strings" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + grypeDb "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" + "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" + "github.com/anchore/syft/syft/linux" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +const ( + branch = "├──" + leaf = "└──" ) type VulnerabilityMatcher struct { @@ -18,6 +36,7 @@ type VulnerabilityMatcher struct { IgnoreRules []match.IgnoreRule FailSeverity *vulnerability.Severity NormalizeByCVE bool + VexProcessor *vex.Processor } func DefaultVulnerabilityMatcher(store store.Store) *VulnerabilityMatcher { @@ -43,8 +62,32 @@ func (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) * } func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Context) (*match.Matches, []match.IgnoredMatch, error) { + progressMonitor := trackMatcher(len(pkgs)) + + defer progressMonitor.SetCompleted() + + remainingMatches, ignoredMatches, err := m.findDBMatches(pkgs, context, progressMonitor) + if err != nil { + return remainingMatches, ignoredMatches, err + } + + remainingMatches, ignoredMatches, err = m.findVEXMatches(context, remainingMatches, ignoredMatches, progressMonitor) + if err != nil { + return remainingMatches, ignoredMatches, fmt.Errorf("unable to find matches against VEX sources: %w", err) + } + + logListSummary(progressMonitor) + + logIgnoredMatches(ignoredMatches) + + return remainingMatches, ignoredMatches, nil +} + +func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Context, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { var ignoredMatches []match.IgnoredMatch - matches := matcher.FindMatches(m.Store, context.Distro, m.Matchers, pkgs) + + log.Trace("finding matches against DB") + matches := m.searchDBForMatches(context.Distro, pkgs, progressMonitor) matches, ignoredMatches = m.applyIgnoreRules(matches) @@ -69,6 +112,85 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte return &matches, ignoredMatches, err } +func (m *VulnerabilityMatcher) searchDBForMatches( + release *linux.Release, + packages []pkg.Package, + progressMonitor *monitorWriter, +) match.Matches { + var err error + res := match.NewMatches() + matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) + + var d *distro.Distro + if release != nil { + d, err = distro.NewFromRelease(*release) + if err != nil { + log.Warnf("unable to determine linux distribution: %+v", err) + } + if d != nil && d.Disabled() { + log.Warnf("unsupported linux distribution: %s", d.Name()) + return match.NewMatches() + } + } + + if defaultMatcher == nil { + defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) + } + for _, p := range packages { + progressMonitor.PackagesProcessed.Increment() + log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches") + + matchAgainst, ok := matcherIndex[p.Type] + if !ok { + matchAgainst = []matcher.Matcher{defaultMatcher} + } + for _, theMatcher := range matchAgainst { + matches, err := theMatcher.Match(m.Store, d, p) + if err != nil { + log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher failed") + } else { + // Filter out matches based on records in the database exclusion table and hard-coded rules + filtered, dropped := match.ApplyExplicitIgnoreRules(m.Store, match.NewMatches(matches...)) + + additionalMatches := filtered.Sorted() + logPackageMatches(p, additionalMatches) + logExplicitDroppedPackageMatches(p, dropped) + res.Add(additionalMatches...) + + progressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches))) + + // note: there is a difference between "ignore" and "dropped" matches. + // ignored: matches that are filtered out due to user-provided ignore rules + // dropped: matches that are filtered out due to hard-coded rules + updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.Store) + } + } + } + + return res +} + +func (m *VulnerabilityMatcher) findVEXMatches(context pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { + if m.VexProcessor == nil { + log.Trace("no VEX documents provided, skipping VEX matching") + return remainingMatches, ignoredMatches, nil + } + + log.Trace("finding matches against available VEX documents") + matchesAfterVex, ignoredMatchesAfterVex, err := m.VexProcessor.ApplyVEX(&context, remainingMatches, ignoredMatches) + if err != nil { + return nil, nil, fmt.Errorf("unable to find matches against VEX documents: %w", err) + } + + diffMatches := matchesAfterVex.Diff(*remainingMatches) + // note: this assumes that the diff can only be additive + diffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches) + + updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.Store) + + return matchesAfterVex, ignoredMatchesAfterVex, nil +} + func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) { var ignoredMatches []match.IgnoredMatch if len(m.IgnoreRules) == 0 { @@ -98,12 +220,19 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { switch len(effectiveCVERecordRefs) { case 0: - // TODO: trace logging + log.WithFields( + "vuln", match.Vulnerability.ID, + "package", displayPackage(match.Package), + ).Trace("unable to find CVE record for vulnerability, skipping normalization") return match case 1: break default: - // TODO: trace logging + log.WithFields( + "refs", fmt.Sprintf("%+v", effectiveCVERecordRefs), + "vuln", match.Vulnerability.ID, + "package", displayPackage(match.Package), + ).Trace("found multiple CVE records for vulnerability, skipping normalization") return match } @@ -111,7 +240,7 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { upstreamMetadata, err := m.Store.GetMetadata(ref.ID, ref.Namespace) if err != nil { - log.Warnf("unable to fetch effective CVE metadata for id=%q namespace=%q : %v", ref.ID, ref.Namespace, err) + log.WithFields("id", ref.ID, "namespace", ref.Namespace, "error", err).Warn("unable to fetch effective CVE metadata") return match } @@ -131,6 +260,53 @@ func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { return match } +func displayPackage(p pkg.Package) string { + if p.PURL != "" { + return p.PURL + } + return fmt.Sprintf("%s@%s (%s)", p.Name, p.Version, p.Type) +} + +func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch) []match.IgnoredMatch { + // TODO(alex): the downside with this implementation is that it does not account for the same ignored match being + // ignored for different reasons (the appliedIgnoreRules field). + + otherMap := make(map[match.Fingerprint]struct{}) + for _, a := range other { + otherMap[a.Match.Fingerprint()] = struct{}{} + } + + var diff []match.IgnoredMatch + for _, b := range subject { + if _, ok := otherMap[b.Match.Fingerprint()]; !ok { + diff = append(diff, b) + } + } + + return diff +} + +func newMatcherIndex(matchers []matcher.Matcher) (map[syftPkg.Type][]matcher.Matcher, matcher.Matcher) { + matcherIndex := make(map[syftPkg.Type][]matcher.Matcher) + var defaultMatcher matcher.Matcher + for _, m := range matchers { + if m.Type() == match.StockMatcher { + defaultMatcher = m + continue + } + for _, t := range m.PackageTypes() { + if _, ok := matcherIndex[t]; !ok { + matcherIndex[t] = make([]matcher.Matcher, 0) + } + + matcherIndex[t] = append(matcherIndex[t], m) + log.Debugf("adding matcher: %+v", t) + } + } + + return matcherIndex, defaultMatcher +} + func isCVE(id string) bool { return strings.HasPrefix(strings.ToLower(id), "cve-") } @@ -151,3 +327,154 @@ func HasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnera } return false } + +func logListSummary(vl *monitorWriter) { + log.Infof("found %d vulnerability matches across %d packages", vl.MatchesDiscovered.Current(), vl.PackagesProcessed.Current()) + log.Debugf(" ├── fixed: %d", vl.Fixed.Current()) + log.Debugf(" ├── ignored: %d (due to user-provided rule)", vl.Ignored.Current()) + log.Debugf(" ├── dropped: %d (due to hard-coded correction)", vl.Dropped.Current()) + log.Debugf(" └── matched: %d", vl.MatchesDiscovered.Current()) + + var unknownCount int64 + if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok { + unknownCount = count.Current() + } + log.Debugf(" ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount) + + allSeverities := vulnerability.AllSeverities() + for idx, sev := range allSeverities { + arm := selectArm(idx, len(allSeverities)) + log.Debugf(" %s %s: %d", arm, sev.String(), vl.BySeverity[sev].Current()) + } +} + +func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) { + for _, m := range matches { + metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) + if err != nil || metadata == nil { + mon.BySeverity[vulnerability.UnknownSeverity].Increment() + continue + } + + sevManualProgress, ok := mon.BySeverity[vulnerability.ParseSeverity(metadata.Severity)] + if !ok { + mon.BySeverity[vulnerability.UnknownSeverity].Increment() + continue + } + sevManualProgress.Increment() + + if m.Vulnerability.Fix.State == grypeDb.FixedState { + mon.Fixed.Increment() + } + } + + mon.Ignored.Add(int64(len(ignores))) + mon.Dropped.Add(int64(len(dropped))) +} + +func logPackageMatches(p pkg.Package, matches []match.Match) { + if len(matches) == 0 { + return + } + + log.WithFields("package", displayPackage(p)).Debugf("found %d vulnerabilities", len(matches)) + for idx, m := range matches { + arm := selectArm(idx, len(matches)) + log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Debugf(" %s", arm) + } +} + +func selectArm(idx, total int) string { + if idx == total-1 { + return leaf + } + return branch +} + +func logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatch) { + if len(ignored) == 0 { + return + } + + log.WithFields("package", displayPackage(p)).Debugf("dropped %d vulnerability matches due to hard-coded correction", len(ignored)) + for idx, i := range ignored { + arm := selectArm(idx, len(ignored)) + + log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Debugf(" %s", arm) + } +} + +func logIgnoredMatches(ignored []match.IgnoredMatch) { + if len(ignored) == 0 { + return + } + + log.Infof("ignored %d vulnerability matches", len(ignored)) + for idx, i := range ignored { + arm := selectArm(idx, len(ignored)) + + log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules), "package", displayPackage(i.Package)).Debugf(" %s", arm) + } +} + +type monitorWriter struct { + PackagesProcessed *progress.Manual + MatchesDiscovered *progress.Manual + Fixed *progress.Manual + Ignored *progress.Manual + Dropped *progress.Manual + BySeverity map[vulnerability.Severity]*progress.Manual +} + +func newMonitor(pkgCount int) (monitorWriter, monitor.Matching) { + manualBySev := make(map[vulnerability.Severity]*progress.Manual) + for _, severity := range vulnerability.AllSeverities() { + manualBySev[severity] = progress.NewManual(-1) + } + manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1) + + m := monitorWriter{ + PackagesProcessed: progress.NewManual(int64(pkgCount)), + MatchesDiscovered: progress.NewManual(-1), + Fixed: progress.NewManual(-1), + Ignored: progress.NewManual(-1), + Dropped: progress.NewManual(-1), + BySeverity: manualBySev, + } + + monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable) + for sev, manual := range manualBySev { + monitorableBySev[sev] = manual + } + + return m, monitor.Matching{ + PackagesProcessed: m.PackagesProcessed, + MatchesDiscovered: m.MatchesDiscovered, + Fixed: m.Fixed, + Ignored: m.Ignored, + Dropped: m.Dropped, + BySeverity: monitorableBySev, + } +} + +func (m *monitorWriter) SetCompleted() { + m.PackagesProcessed.SetCompleted() + m.MatchesDiscovered.SetCompleted() + m.Fixed.SetCompleted() + m.Ignored.SetCompleted() + m.Dropped.SetCompleted() + for _, v := range m.BySeverity { + v.SetCompleted() + } +} + +func trackMatcher(pkgCount int) *monitorWriter { + writer, reader := newMonitor(pkgCount) + + bus.Publish(partybus.Event{ + Type: event.VulnerabilityScanningStarted, + Value: reader, + }) + + return &writer +} diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index b10e3e3c233..8b83b63b7b9 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/facebookincubator/nvdtools/wfn" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" @@ -15,10 +16,12 @@ import ( "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" + "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/source" @@ -653,6 +656,49 @@ func TestMatchByImage(t *testing.T) { }) } + // Test that VEX matchers produce matches when fed documents with "affected" + // statuses. + for n, tc := range map[string]struct { + vexStatus vex.Status + vexDocuments []string + }{ + "openvex-affected": {vex.StatusAffected, []string{"test-fixtures/vex/openvex/affected.openvex.json"}}, + "openvex-under_investigation": {vex.StatusUnderInvestigation, []string{"test-fixtures/vex/openvex/under_investigation.openvex.json"}}, + } { + t.Run(n, func(t *testing.T) { + ignoredMatches := testIgnoredMatches() + vexedResults := vexMatches(t, ignoredMatches, tc.vexStatus, tc.vexDocuments) + if len(vexedResults.Sorted()) != 1 { + t.Errorf("expected one vexed result, got none") + } + + expectedMatches := match.NewMatches() + + // The single match in the actual results is the same in ignoredMatched + // but must the details of the VEX matcher appended + result := vexedResults.Sorted()[0] + if len(result.Details) != len(ignoredMatches[0].Match.Details)+1 { + t.Errorf( + "Details in VEXed results don't match (expected %d, got %d)", + len(ignoredMatches[0].Match.Details)+1, len(result.Details), + ) + } + + result.Details = result.Details[:len(result.Details)-1] + actualResults := match.NewMatches() + actualResults.Add(result) + + expectedMatches.Add(ignoredMatches[0].Match) + assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted()) + + for _, m := range vexedResults.Sorted() { + for _, d := range m.Details { + observedMatchers.Add(string(d.Matcher)) + } + } + }) + } + // ensure that integration test cases stay in sync with the implemented matchers observedMatchers.Remove(string(match.StockMatcher)) definedMatchers.Remove(string(match.StockMatcher)) @@ -670,6 +716,95 @@ func TestMatchByImage(t *testing.T) { } +// testIgnoredMatches returns an list of ignored matches to test the vex +// matchers +func testIgnoredMatches() []match.IgnoredMatch { + return []match.IgnoredMatch{ + { + Match: match.Match{ + Vulnerability: vulnerability.Vulnerability{ + ID: "CVE-alpine-libvncserver", + Namespace: "alpine:distro:alpine:3.12", + }, + Package: pkg.Package{ + ID: "44fa3691ae360cac", + Name: "libvncserver", + Version: "0.9.9", + Licenses: []string{"GPL-2.0-or-later"}, + Type: "apk", + CPEs: []wfn.Attributes{ + { + Part: "a", + Vendor: "libvncserver", + Product: "libvncserver", + Version: "0.9.9", + }, + }, + PURL: "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0", + Upstreams: []pkg.UpstreamPackage{{Name: "libvncserver"}}, + }, + Details: []match.Detail{ + { + 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, + }, + }, + }, + AppliedIgnoreRules: []match.IgnoreRule{}, + }, + } +} + +// vexMatches moves the first match of a matches list to an ignore list and +// applies a VEX "affected" document to it to move it to the matches list. +func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex.Status, vexDocuments []string) match.Matches { + matches := match.NewMatches() + vexMatcher := vex.NewProcessor(vex.ProcessorOptions{ + Documents: vexDocuments, + IgnoreRules: []match.IgnoreRule{ + {VexStatus: string(vexStatus)}, + }, + }) + + pctx := &pkg.Context{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ + RepoDigests: []string{ + "alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }, + }, + Distro: &linux.Release{}, + } + + vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches) + if err != nil { + t.Errorf("applying VEX data: %s", err) + } + + if len(ignoredMatches) != 0 { + t.Errorf("VEX text fixture %s must affect all ignored matches (%d left)", vexDocuments, len(ignoredMatches)) + } + + return *vexedMatches +} + func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() var opts = []cmp.Option{ diff --git a/test/integration/test-fixtures/vex/openvex/affected.openvex.json b/test/integration/test-fixtures/vex/openvex/affected.openvex.json new file mode 100644 index 00000000000..78b24f5b80b --- /dev/null +++ b/test/integration/test-fixtures/vex/openvex/affected.openvex.json @@ -0,0 +1,23 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "The OpenVEX Project ", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-alpine-libvncserver" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } + ] + } + ], + "status": "affected" + } + ] +} diff --git a/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json b/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json new file mode 100644 index 00000000000..f9e4c60e38e --- /dev/null +++ b/test/integration/test-fixtures/vex/openvex/under_investigation.openvex.json @@ -0,0 +1,24 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "The OpenVEX Project ", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "timestamp": "2023-07-16T18:28:47.696004345-06:00", + "vulnerability": { + "name": "CVE-alpine-libvncserver" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } + ] + } + ], + "status": "under_investigation" + } + ] +}