Skip to content

Commit

Permalink
Ignore/add match results based on OpenVEX documents (#1397)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
puerco and wagoodman committed Sep 13, 2023
1 parent 6ee9054 commit b952d38
Show file tree
Hide file tree
Showing 37 changed files with 1,920 additions and 429 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.tool-versions
/go.work
/go.work.sum
/.grype.yaml
Expand Down
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions cmd/grype/cli/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
9 changes: 9 additions & 0 deletions cmd/grype/cli/options/grype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
---
31 changes: 23 additions & 8 deletions cmd/grype/cli/ui/handle_vulnerability_scanning_started.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}
Expand Down
32 changes: 26 additions & 6 deletions cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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: {},
Expand All @@ -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,
}
}
Loading

0 comments on commit b952d38

Please sign in to comment.