mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
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:
parent
6ee9054c88
commit
b952d3808c
37 changed files with 1921 additions and 430 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/.tool-versions
|
||||
/go.work
|
||||
/go.work.sum
|
||||
/.grype.yaml
|
||||
|
|
76
README.md
76
README.md
|
@ -46,6 +46,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
|
|||
- PHP (Composer)
|
||||
- Rust (Cargo)
|
||||
- Supports Docker, OCI and [Singularity](https://github.com/sylabs/singularity) image formats.
|
||||
- [OpenVEX](https://github.com/openvex) support for filtering and augmenting scanning results.
|
||||
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/grype/issues).
|
||||
|
||||
|
@ -322,6 +323,9 @@ ignore:
|
|||
# This is the full set of supported rule fields:
|
||||
- vulnerability: CVE-2008-4318
|
||||
fix-state: unknown
|
||||
# VEX fields apply when Grype reads vex data:
|
||||
vex-status: not_affected
|
||||
vex-justification: vulnerable_code_not_present
|
||||
package:
|
||||
name: libcurl
|
||||
version: 1.5.1
|
||||
|
@ -370,6 +374,78 @@ apk-tools 2.10.6-r0 2.10.7-r0 CVE-2021-36159 Critical
|
|||
|
||||
If you want Grype to only report vulnerabilities **that do not have a confirmed fix**, you can use the `--only-notfixed` flag. (This automatically adds [ignore rules](#specifying-matches-to-ignore) into Grype's configuration, such that vulnerabilities that are fixed will be ignored.)
|
||||
|
||||
## VEX Support
|
||||
|
||||
Grype can use VEX (Vulnerability Exploitability Exchange) data to filter false
|
||||
positives or provide additional context, augmenting matches. When scanning a
|
||||
container image, you can use the `--vex` flag to point to one or more
|
||||
[OpenVEX](https://github.com/openvex) documents.
|
||||
|
||||
VEX statements relate a product (a container image), a vulnerability, and a VEX
|
||||
status to express an assertion of the vulnerability's impact. There are four
|
||||
[VEX statuses](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-labels):
|
||||
`not_affected`, `affected`, `fixed` and `under_investigation`.
|
||||
|
||||
Here is an example of a simple OpenVEX document. (tip: use
|
||||
[`vexctl`](https://github.com/openvex/vexctl) to generate your own documents).
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78",
|
||||
"author": "A Grype User <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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
---
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
38
go.mod
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/gkampitakis/go-snaps v0.4.10
|
||||
github.com/go-test/deep v1.1.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/go-containerregistry v0.16.1
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
|
@ -40,6 +41,7 @@ require (
|
|||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/openvex/go-vex v0.2.5
|
||||
github.com/owenrumney/go-sarif v1.1.1
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||
|
@ -57,21 +59,20 @@ require (
|
|||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
golang.org/x/term v0.12.0 // indirect
|
||||
gorm.io/gorm v1.23.10
|
||||
modernc.org/sqlite v1.25.0
|
||||
)
|
||||
|
||||
require modernc.org/sqlite v1.25.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.19.3 // indirect
|
||||
cloud.google.com/go v0.110.2 // indirect
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
cloud.google.com/go/storage v1.28.1 // indirect
|
||||
cloud.google.com/go/iam v1.1.0 // indirect
|
||||
cloud.google.com/go/storage v1.29.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||
github.com/acobaugh/osrelease v0.1.0 // indirect
|
||||
|
@ -82,7 +83,7 @@ require (
|
|||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.180 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.288 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
|
@ -116,12 +117,11 @@ require (
|
|||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-containerregistry v0.16.1 // indirect
|
||||
github.com/google/licensecheck v0.3.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||
github.com/google/s2a-go v0.1.3 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
|
@ -154,15 +154,17 @@ require (
|
|||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.19.0 // indirect
|
||||
github.com/onsi/gomega v1.27.4 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.1 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
|
@ -211,11 +213,13 @@ require (
|
|||
golang.org/x/time v0.2.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.122.0 // indirect
|
||||
google.golang.org/api v0.128.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.56.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
69
go.sum
69
go.sum
|
@ -33,8 +33,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
|
|||
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
||||
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
||||
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
|
||||
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
|
||||
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
|
||||
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
|
||||
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
|
||||
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
|
||||
|
@ -71,8 +71,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz
|
|||
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
|
||||
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
|
||||
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
|
||||
cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
|
||||
cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
|
||||
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
|
||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
|
||||
|
@ -113,14 +113,12 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97
|
|||
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
|
||||
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
|
||||
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
|
||||
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
|
||||
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
|
||||
cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94=
|
||||
cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk=
|
||||
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
|
||||
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
|
||||
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
|
||||
cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
|
||||
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
|
||||
cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
|
||||
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
|
||||
|
@ -178,8 +176,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
|
||||
cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
|
||||
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
|
||||
|
@ -209,8 +207,9 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ
|
|||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
|
@ -275,8 +274,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
|||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI=
|
||||
github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q=
|
||||
github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
|
@ -525,8 +524,8 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I
|
|||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
|
||||
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -535,8 +534,8 @@ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
|
@ -546,8 +545,8 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99
|
|||
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
||||
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
|
||||
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
|
||||
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
|
@ -734,8 +733,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
|||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
|
@ -766,14 +765,18 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
|||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
|
||||
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
|
||||
github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
|
||||
github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
|
||||
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
|
||||
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
|
||||
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
|
||||
|
@ -1391,8 +1394,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ
|
|||
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
|
||||
google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg=
|
||||
google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -1508,8 +1511,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw
|
|||
google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
|
||||
google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
|
||||
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
@ -1546,8 +1553,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu
|
|||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE=
|
||||
google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
@ -1564,8 +1571,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
29
grype/presenter/table/__snapshots__/presenter_test.snap
Executable file
29
grype/presenter/table/__snapshots__/presenter_test.snap
Executable 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)
|
||||
|
||||
---
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -1 +0,0 @@
|
|||
No vulnerabilities found
|
|
@ -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
|
|
@ -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
|
324
grype/vex/openvex/implementation.go
Normal file
324
grype/vex/openvex/implementation.go
Normal 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
|
||||
}
|
38
grype/vex/openvex/implementation_test.go
Normal file
38
grype/vex/openvex/implementation_test.go
Normal 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
112
grype/vex/processor.go
Normal 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
314
grype/vex/processor_test.go
Normal 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)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
24
grype/vex/testdata/vex-docs/openvex-demo1.json
vendored
Normal file
24
grype/vex/testdata/vex-docs/openvex-demo1.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
89
grype/vex/testdata/vex-docs/openvex-demo2.json
vendored
Normal file
89
grype/vex/testdata/vex-docs/openvex-demo2.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue