Ignore/add match results based on OpenVEX documents (#1397)

* go.mod: Pull OpenVEX go modules

This commit pulls the OpenVEX libraries into the grype source.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add generic VEX processor package

This commit adds a generic VEX processor package. It is implementation
agnostic. It has a single option for now: The documents used to load
the VEX data.

The processor has a single method: ApplyVEX() which takes a set of scan
results and applies VEX data to them. For now, the only modification that
is done is filtering of results, that is moving results to the ignored list
as a response to VEX documents.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* vex: Add OpenVEX processor implementation

This commit adds an openvex implementation of the vex processor.
It also wires the VEX processor to use it as default.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Table presenter: Highligt results suppressed by VEX

This commit marks results suppressed by VEX when presenting them
to the user.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Define  VEX status constants

This commit defines a set of local constants of each of the VEX statuses
based on the openvex constants.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add VexStatus to ignore rules

This commit modifies the ignore rules structure to support defining a vex
status. Any rules defining vex are ignored by the standard ignore rules
processing as they will be handled by the VEX processor.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add IgnoreRule HasConditions method

Adds a new HasConditions method to the IgnoreRule object to check if the rule is empty.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Control VEX filtering through IgnoreRules

This commit modifies how the vex processor is controlled. The processor now
takes a list of IgnoreRules which can act on the VEX status in addition to
the regular rule parameters.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* vex: Allow rules to match on VEX justification

This commit expands the ingore rules to also work on vex the
justification of not_affected statements.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Use go-vex merge implementation

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add OpenVEX matcher to matcher list

This commit adds a new entry to the matchers: An openvex matcher

This matcher is used when openvex augments results, moving matches
from the ignore list to the active results.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add vex.AugmentMatches() to the vex processor

This commit adds a new AugmentMatches() phase to the VEX processor.

This new step goes throught the configured ignore rules and acts on any
that have `affected` or `under_investigtion` as status.

The purpose of this rule is to move matches back from the ignored matches
list to the active results when a statement with either of those statuses
apply to ignored matches.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Parse context identifiers using GGC

This commit modifies the identifier synthesizer function to parse references
using GGCR. It also adds a simple test.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Bump funlen linter to 73

This commit bumps the maximum function length to 73 to accomodate
the new flag in AddFlags()

Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@chainguard.dev>

* Add VEX testing to matchers test

This commit adds a new test and fixtures to test the VEX matchers
along the rest of the matchers in TestMatchByImage(). As the VEX
matchers operate on previously ignored matches a new loop was added
to the test to accomodate the different testing model.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* add vex status and justification to ignored rule json model

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* nit rename + add TODO question about augmenting ignored matches

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* nit document comment updates + common variable extraction

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* migrate legacy matcher function to vulnerability matcher object

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update tui to respond to ignored and dropped matches

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* migrate vex processing to vulnerability match object

Based on Alex's previous caommit

Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Migrate VEX options and app config from legacy CLI

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* update table snapshot tests with suppressed vex entries

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for match.Matches.Diff()

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for vex processor

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting and restore global funlen rule

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove grpc pin

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* always return remaining and ignroed matches from matcher object

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* Add VEX documentation to main README

This commit adds a VEX section to the main Grype README. It adds
an example document and details on how vex rules can be written.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

---------

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>
Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@chainguard.dev>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Puerco 2023-09-13 12:26:12 -07:00 committed by GitHub
parent 6ee9054c88
commit b952d3808c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1921 additions and 430 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/.tool-versions
/go.work
/go.work.sum
/.grype.yaml

View file

@ -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 <jdoe@example.com>",
"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:

View file

@ -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
}

View file

@ -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 {

View file

@ -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)
---

View file

@ -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()
}

View file

@ -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,
}
}

38
go.mod
View file

@ -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

69
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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)
})
}
}

View file

@ -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())
}
}
}

View file

@ -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{},
},
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1999-0002",
Namespace: "source-2",
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",
},
},
},
},
Package: p,
Details: []match.Detail{
AppliedIgnoreRules: []match.IgnoreRule{},
},
{
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",
},
},
},
},
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 {

View file

@ -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,
}
}

View file

@ -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",
},
},
},
},
},
},
}
}

View file

@ -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)
---

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -1 +0,0 @@
No vulnerabilities found

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}

112
grype/vex/processor.go Normal file
View file

@ -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
}

314
grype/vex/processor_test.go Normal file
View file

@ -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)
})
}
}

View file

@ -0,0 +1,24 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
"author": "The OpenVEX Project <openvex@openssf.org>",
"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"
}
]
}

View file

@ -0,0 +1,89 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
"author": "The OpenVEX Project <openvex@openssf.org>",
"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"
}
]
}

View file

@ -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
}

View file

@ -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{

View file

@ -0,0 +1,23 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
"author": "The OpenVEX Project <openvex@openssf.org>",
"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"
}
]
}

View file

@ -0,0 +1,24 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
"author": "The OpenVEX Project <openvex@openssf.org>",
"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"
}
]
}