Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore/add match results based on OpenVEX documents #1397

Merged
merged 29 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ccc0e33
go.mod: Pull OpenVEX go modules
puerco Jul 19, 2023
c674627
Add generic VEX processor package
puerco Jul 20, 2023
c4064ba
vex: Add OpenVEX processor implementation
puerco Jul 20, 2023
3de9d5e
Table presenter: Highligt results suppressed by VEX
puerco Jul 20, 2023
13e4c65
Define VEX status constants
puerco Jul 20, 2023
123da3b
Add VexStatus to ignore rules
puerco Jul 20, 2023
836f616
Add IgnoreRule HasConditions method
puerco Jul 20, 2023
4468b51
Control VEX filtering through IgnoreRules
puerco Jul 20, 2023
5fc15f8
vex: Allow rules to match on VEX justification
puerco Jul 20, 2023
0669737
Use go-vex merge implementation
puerco Aug 18, 2023
7b1a7a1
Add OpenVEX matcher to matcher list
puerco Aug 23, 2023
e50cb32
Add vex.AugmentMatches() to the vex processor
puerco Aug 23, 2023
a4e71de
Parse context identifiers using GGC
puerco Aug 23, 2023
ae54e93
Bump funlen linter to 73
puerco Aug 25, 2023
ba27ba3
Add VEX testing to matchers test
puerco Aug 31, 2023
6af9fc9
add vex status and justification to ignored rule json model
wagoodman Sep 8, 2023
1f17f91
nit rename + add TODO question about augmenting ignored matches
wagoodman Sep 8, 2023
aafb030
nit document comment updates + common variable extraction
wagoodman Sep 8, 2023
307fa7c
migrate legacy matcher function to vulnerability matcher object
wagoodman Sep 8, 2023
2e6cc2e
update tui to respond to ignored and dropped matches
wagoodman Sep 8, 2023
675713d
migrate vex processing to vulnerability match object
puerco Sep 12, 2023
21a5072
Migrate VEX options and app config from legacy CLI
puerco Sep 12, 2023
d086099
update table snapshot tests with suppressed vex entries
wagoodman Sep 12, 2023
803d15b
add tests for match.Matches.Diff()
wagoodman Sep 12, 2023
655b7d6
add tests for vex processor
wagoodman Sep 12, 2023
43a246e
fix linting and restore global funlen rule
wagoodman Sep 12, 2023
893c414
remove grpc pin
wagoodman Sep 12, 2023
3bacab8
always return remaining and ignroed matches from matcher object
wagoodman Sep 12, 2023
21ed51b
Add VEX documentation to main README
puerco Sep 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>",
"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/[email protected]" },
{ "@id": "pkg:apk/alpine/[email protected]" }
]
}
],
"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