Feature: Specifying ignore rules for vulnerability matches (#430)

* Preliminary implementation of ignore rules

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Support ignoring matches by package type

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Add tests for ignore functionality

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Add documentation for ignore rules and clean up README

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Add test for glob location matching

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
This commit is contained in:
Dan Luhring 2021-09-29 15:44:36 -04:00 committed by GitHub
parent e6831d9444
commit f86fd7eb38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 750 additions and 30 deletions

View file

@ -55,6 +55,7 @@ To include software from all image layers in the vulnerability scan, regardless
```
grype <image> --scope all-layers
```
### Supported sources
Grype can scan a variety of sources beyond those found in Docker.
@ -89,6 +90,8 @@ dir:path/to/yourproject read directly from a path on disk (any di
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
```
### Output formats
The output format for Grype is configurable as well:
```
grype <image> -o <format>
@ -98,9 +101,9 @@ Where the `format`s available are:
- `table`: A columnar summary (default).
- `cyclonedx`: An XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
- `json`: Use this to get as much information out of Grype as possible!
- `template`: Lets the user specify the output format. See [Using Templates](#using-templates) below.
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
### Using Templates
### Using templates
Grype lets you define custom output formats, using [Go templates](https://golang.org/pkg/text/template/). Here's how it works:
@ -131,7 +134,60 @@ Which would produce output like:
...
```
### Grype's Database
### Gating on severity of vulnerabilities
You can have Grype exit with an error if any vulnerabilities are reported at or above the specified severity level. This comes in handy when using Grype within a script or CI pipeline. To do this, use the `--fail-on <severity>` CLI flag.
For example, here's how you could trigger a CI pipeline failure if any vulnerabilities are found in the `ubuntu:latest` image with a severity of "medium" or higher:
```
grype ubuntu:latest --fail-on medium
```
### Specifying matches to ignore
If you're seeing Grype report **false positives** or any other vulnerability matches that you just don't want to see, you can tell Grype to **ignore** matches by specifying one or more _"ignore rules"_ in your Grype configuration file (e.g. `~/.grype.yaml`). This causes Grype not to report any vulnerability matches that meet the criteria specified by any of your ignore rules.
Each rule can specify any combination of the following criteria:
- vulnerability ID (e.g. `"CVE-2008-4318"`)
- package name (e.g. `"libcurl"`)
- package version (e.g. `"1.5.1"`)
- package type (e.g. `"npm"`; these values are defined [here](https://github.com/anchore/syft/blob/main/syft/pkg/type.go#L10-L21))
- package location (e.g. `"/usr/local/lib/node_modules/**"`; supports glob patterns)
Here's an example `~/.grype.yaml` that demonstrates the expected format for ignore rules:
```yaml
ignore:
# This is the full set of supported rule fields:
- vulnerability: CVE-2008-4318
package:
name: libcurl
version: 1.5.1
type: npm
location: "/usr/local/lib/node_modules/**"
# We can make rules to match just by vulnerability ID:
- vulnerability: CVE-2017-41432
# ...or just by a single package field:
- package:
type: gem
```
Vulnerability matches will be ignored if **any** rules apply to the match. A rule is considered to apply to a given vulnerability match only if **all** fields specified in the rule apply to the vulnerability match.
When you run Grype while specifying ignore rules, the following happens to the vulnerability matches that are "ignored":
- Ignored matches are **completely hidden** from Grype's output, except for when using the `json` or `template` output formats; however, in these two formats, the ignored matches are **removed** from the existing `matches` array field, and they are placed in a new `ignoredMatches` array field. Each listed ignored match also has an additional field, `appliedIgnoreRules`, which is an array of any rules that caused Grype to ignore this vulnerability match.
- Ignored matches **do not** factor into Grype's exit status decision when using `--fail-on <severity>`. For instance, if a user specifies `--fail-on critical`, and all of the vulnerability matches found with a "critical" severity have been _ignored_, Grype will exit zero.
**Note:** Please continue to **[report](https://github.com/anchore/grype/issues/new/choose)** any false positives you see! Even if you can reliably filter out false positives using ignore rules, it's very helpful to the Grype community if we have as much knowledge about Grype's false positives as possible. This helps us continuously improve Grype!
### Grype's database
Grype pulls a database of vulnerabilities derived from the publicly available [Anchore Feed Service](https://ancho.re/v1/service/feeds). This database is updated at the beginning of each scan, but an update can also be triggered manually.
@ -158,7 +214,7 @@ brew tap anchore/grype
brew install grype
```
## Shell Completion
## Shell completion
Grype supplies shell completion through its CLI implementation ([cobra](https://github.com/spf13/cobra/blob/master/shell_completions.md)). Generate the completion code for your shell by running one of the following commands:

View file

@ -228,18 +228,23 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
return
}
matches := grype.FindVulnerabilitiesForPackage(provider, context.Distro, packages...)
allMatches := grype.FindVulnerabilitiesForPackage(provider, context.Distro, packages...)
remainingMatches, ignoredMatches := match.ApplyIgnoreRules(allMatches, appConfig.Ignore)
if count := len(ignoredMatches); count > 0 {
log.Infof("Ignoring %d matches due to user-provided ignore rules", count)
}
// determine if there are any severities >= to the max allowable severity (which is optional).
// note: until the shared file lock in sqlittle is fixed the sqlite DB cannot be access concurrently,
// implying that the fail-on-severity check must be done before sending the presenter object.
if hitSeverityThreshold(failOnSeverity, matches, metadataProvider) {
if hitSeverityThreshold(failOnSeverity, remainingMatches, metadataProvider) {
errs <- grypeerr.ErrAboveSeverityThreshold
}
bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter.GetPresenter(presenterConfig, matches, packages, context, metadataProvider, appConfig, dbStatus),
Value: presenter.GetPresenter(presenterConfig, remainingMatches, ignoredMatches, packages, context, metadataProvider, appConfig, dbStatus),
})
}()
return errs

2
go.mod
View file

@ -10,10 +10,12 @@ require (
github.com/anchore/grype-db v0.0.0-20210928194208-f146397d6cd0
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a
github.com/anchore/syft v0.24.1
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.7
github.com/google/go-cmp v0.4.1
github.com/google/uuid v1.1.1
github.com/gookit/color v1.2.7
github.com/hashicorp/go-getter v1.4.1

167
grype/match/ignore.go Normal file
View file

@ -0,0 +1,167 @@
package match
import "github.com/bmatcuk/doublestar/v2"
// An IgnoredMatch is a vulnerability Match that has been ignored because one or more IgnoreRules applied to the match.
type IgnoredMatch struct {
Match
// AppliedIgnoreRules are the rules that were applied to the match that caused Grype to ignore it.
AppliedIgnoreRules []IgnoreRule
}
// An IgnoreRule specifies criteria for a vulnerability match to meet in order
// to be ignored. Not all criteria (fields) need to be specified, but all
// 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"`
Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"`
}
// IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule.
type IgnoreRulePackage struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Version string `yaml:"version" json:"version" mapstructure:"version"`
Type string `yaml:"type" json:"type" mapstructure:"type"`
Location string `yaml:"location" json:"location" mapstructure:"location"`
}
// ApplyIgnoreRules iterates through the provided matches and, for each match,
// determines if the match should be ignored, by evaluating if any of the
// provided IgnoreRules apply to the match. If any rules apply to the match, all
// applicable rules are attached to the Match to form an IgnoredMatch.
// ApplyIgnoreRules returns two collections: the matches that are not being
// ignored, and the matches that are being ignored.
func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMatch) {
if len(rules) == 0 {
return matches, nil
}
var ignoredMatches []IgnoredMatch
remainingMatches := NewMatches()
for match := range matches.Enumerate() {
var applicableRules []IgnoreRule
for _, rule := range rules {
if shouldIgnore(match, rule) {
applicableRules = append(applicableRules, rule)
}
}
if len(applicableRules) > 0 {
ignoredMatches = append(ignoredMatches, IgnoredMatch{
Match: match,
AppliedIgnoreRules: applicableRules,
})
continue
}
remainingMatches.add(match.Package.ID, match)
}
return remainingMatches, ignoredMatches
}
func shouldIgnore(match Match, rule IgnoreRule) bool {
ignoreConditions := getIgnoreConditionsForRule(rule)
if len(ignoreConditions) == 0 {
// this rule specifies no criteria, so it doesn't apply to the Match
return false
}
for _, condition := range ignoreConditions {
if !condition(match) {
// as soon as one rule criterion doesn't apply, we know this rule doesn't apply to the Match
return false
}
}
// all criteria specified in the rule apply to this Match
return true
}
// An ignoreCondition is a function that returns a boolean indicating whether
// the given Match should be ignored.
type ignoreCondition func(match Match) bool
func getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition {
var ignoreConditions []ignoreCondition
if v := rule.Vulnerability; v != "" {
ignoreConditions = append(ignoreConditions, ifVulnerabilityApplies(v))
}
if n := rule.Package.Name; n != "" {
ignoreConditions = append(ignoreConditions, ifPackageNameApplies(n))
}
if v := rule.Package.Version; v != "" {
ignoreConditions = append(ignoreConditions, ifPackageVersionApplies(v))
}
if t := rule.Package.Type; t != "" {
ignoreConditions = append(ignoreConditions, ifPackageTypeApplies(t))
}
if l := rule.Package.Location; l != "" {
ignoreConditions = append(ignoreConditions, ifPackageLocationApplies(l))
}
return ignoreConditions
}
func ifVulnerabilityApplies(vulnerability string) ignoreCondition {
return func(match Match) bool {
return vulnerability == match.Vulnerability.ID
}
}
func ifPackageNameApplies(name string) ignoreCondition {
return func(match Match) bool {
return name == match.Package.Name
}
}
func ifPackageVersionApplies(version string) ignoreCondition {
return func(match Match) bool {
return version == match.Package.Version
}
}
func ifPackageTypeApplies(t string) ignoreCondition {
return func(match Match) bool {
return t == string(match.Package.Type)
}
}
func ifPackageLocationApplies(location string) ignoreCondition {
return func(match Match) bool {
return ruleLocationAppliesToMatch(location, match)
}
}
func ruleLocationAppliesToMatch(location string, match Match) bool {
for _, packageLocation := range match.Package.Locations {
if ruleLocationAppliesToPath(location, packageLocation.RealPath) {
return true
}
if ruleLocationAppliesToPath(location, packageLocation.VirtualPath) {
return true
}
}
return false
}
func ruleLocationAppliesToPath(location, path string) bool {
doesMatch, err := doublestar.Match(location, path)
if err != nil {
return false
}
return doesMatch
}

318
grype/match/ignore_test.go Normal file
View file

@ -0,0 +1,318 @@
package match
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/anchore/syft/syft/source"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/stretchr/testify/assert"
)
var (
allMatches = []Match{
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-123",
},
Package: pkg.Package{
Name: "dive",
Version: "0.5.2",
Type: "deb",
Locations: []source.Location{
{
RealPath: "/path/that/has/dive",
},
},
},
},
{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-456",
},
Package: pkg.Package{
Name: "reach",
Version: "100.0.50",
Type: "gem",
Locations: []source.Location{
{
RealPath: "/real/path/with/reach",
VirtualPath: "/virtual/path/that/has/reach",
},
},
},
},
}
)
func TestApplyIgnoreRules(t *testing.T) {
cases := []struct {
name string
allMatches []Match
ignoreRules []IgnoreRule
expectedRemainingMatches []Match
expectedIgnoredMatches []IgnoredMatch
}{
{
name: "no ignore rules",
allMatches: allMatches,
ignoreRules: nil,
expectedRemainingMatches: allMatches,
expectedIgnoredMatches: nil,
},
{
name: "no applicable ignore rules",
allMatches: allMatches,
ignoreRules: []IgnoreRule{
{
Vulnerability: "CVE-789",
},
{
Package: IgnoreRulePackage{
Name: "bashful",
Version: "5",
Type: "npm",
},
},
{
Package: IgnoreRulePackage{
Name: "reach",
Version: "3000",
},
},
},
expectedRemainingMatches: allMatches,
expectedIgnoredMatches: nil,
},
{
name: "ignore all matches",
allMatches: allMatches,
ignoreRules: []IgnoreRule{
{
Vulnerability: "CVE-123",
},
{
Package: IgnoreRulePackage{
Location: "/virtual/path/that/has/reach",
},
},
},
expectedRemainingMatches: nil,
expectedIgnoredMatches: []IgnoredMatch{
{
Match: allMatches[0],
AppliedIgnoreRules: []IgnoreRule{
{
Vulnerability: "CVE-123",
},
},
},
{
Match: allMatches[1],
AppliedIgnoreRules: []IgnoreRule{
{
Package: IgnoreRulePackage{
Location: "/virtual/path/that/has/reach",
},
},
},
},
},
},
{
name: "ignore subset of matches",
allMatches: allMatches,
ignoreRules: []IgnoreRule{
{
Vulnerability: "CVE-456",
},
},
expectedRemainingMatches: []Match{
allMatches[0],
},
expectedIgnoredMatches: []IgnoredMatch{
{
Match: allMatches[1],
AppliedIgnoreRules: []IgnoreRule{
{
Vulnerability: "CVE-456",
},
},
},
},
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
locationComparerOption := cmp.Comparer(func(x, y source.Location) bool {
return x.RealPath == y.RealPath && x.VirtualPath == y.VirtualPath
})
actualRemainingMatches, actualIgnoredMatches := ApplyIgnoreRules(sliceToMatches(testCase.allMatches), testCase.ignoreRules)
if diff := cmp.Diff(testCase.expectedRemainingMatches, matchesToSlice(actualRemainingMatches), locationComparerOption); diff != "" {
t.Errorf("unexpected diff in remaining matches (-expected +actual):\n%s", diff)
}
if diff := cmp.Diff(testCase.expectedIgnoredMatches, actualIgnoredMatches, locationComparerOption); diff != "" {
t.Errorf("unexpected diff in ignored matches (-expected +actual):\n%s", diff)
}
})
}
}
func sliceToMatches(s []Match) Matches {
matches := NewMatches()
matches.add("123", s...)
return matches
}
func matchesToSlice(m Matches) []Match {
slice := m.Sorted()
if len(slice) == 0 {
return nil
}
return slice
}
var (
exampleMatch = Match{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2000-1234",
},
Package: pkg.Package{
Name: "a-pkg",
Version: "1.0",
Locations: []source.Location{
{
RealPath: "/some/path",
},
{
RealPath: "/some/path",
VirtualPath: "/some/virtual/path",
},
},
Type: "rpm",
},
}
)
func TestShouldIgnore(t *testing.T) {
cases := []struct {
name string
match Match
rule IgnoreRule
expected bool
}{
{
name: "empty rule",
match: exampleMatch,
rule: IgnoreRule{},
expected: false,
},
{
name: "rule applies via vulnerability ID",
match: exampleMatch,
rule: IgnoreRule{
Vulnerability: exampleMatch.Vulnerability.ID,
},
expected: true,
},
{
name: "rule applies via package name",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Name: exampleMatch.Package.Name,
},
},
expected: true,
},
{
name: "rule applies via package version",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Version: exampleMatch.Package.Version,
},
},
expected: true,
},
{
name: "rule applies via package type",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Type: string(exampleMatch.Package.Type),
},
},
expected: true,
},
{
name: "rule applies via package location real path",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Location: exampleMatch.Package.Locations[0].RealPath,
},
},
expected: true,
},
{
name: "rule applies via package location virtual path",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Location: exampleMatch.Package.Locations[1].VirtualPath,
},
},
expected: true,
},
{
name: "rule applies via package location glob",
match: exampleMatch,
rule: IgnoreRule{
Package: IgnoreRulePackage{
Location: "/some/**",
},
},
expected: true,
},
{
name: "rule applies via multiple fields",
match: exampleMatch,
rule: IgnoreRule{
Vulnerability: exampleMatch.Vulnerability.ID,
Package: IgnoreRulePackage{
Type: string(exampleMatch.Package.Type),
},
},
expected: true,
},
{
name: "rule doesn't apply despite some fields matching",
match: exampleMatch,
rule: IgnoreRule{
Vulnerability: exampleMatch.Vulnerability.ID,
Package: IgnoreRulePackage{
Name: "not-the-right-package",
Version: exampleMatch.Package.Version,
},
},
expected: false,
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
actual := shouldIgnore(testCase.match, testCase.rule)
assert.Equal(t, testCase.expected, actual)
})
}
}

View file

@ -14,6 +14,7 @@ import (
// Presenter is a generic struct for holding fields needed for reporting
type Presenter struct {
matches match.Matches
ignoredMatches []match.IgnoredMatch
packages []pkg.Package
context pkg.Context
metadataProvider vulnerability.MetadataProvider
@ -22,10 +23,10 @@ type Presenter struct {
}
// NewPresenter is a *Presenter constructor
func NewPresenter(matches match.Matches, packages []pkg.Package, context pkg.Context,
metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) *Presenter {
func NewPresenter(matches match.Matches, ignoredMatches []match.IgnoredMatch, packages []pkg.Package, context pkg.Context, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) *Presenter {
return &Presenter{
matches: matches,
ignoredMatches: ignoredMatches,
packages: packages,
metadataProvider: metadataProvider,
context: context,
@ -36,7 +37,8 @@ func NewPresenter(matches match.Matches, packages []pkg.Package, context pkg.Con
// Present creates a JSON-based reporting
func (pres *Presenter) Present(output io.Writer) error {
doc, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.metadataProvider, pres.appConfig, pres.dbStatus)
doc, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider,
pres.appConfig, pres.dbStatus)
if err != nil {
return err
}

View file

@ -156,7 +156,7 @@ func TestJsonImgsPresenter(t *testing.T) {
Source: &src.Metadata,
Distro: &d,
}
pres := NewPresenter(matches, packages, ctx, models.NewMetadataMock(), nil, nil)
pres := NewPresenter(matches, nil, packages, ctx, models.NewMetadataMock(), nil, nil)
// TODO: add a constructor for a match.Match when the data is better shaped
@ -300,7 +300,7 @@ func TestJsonDirsPresenter(t *testing.T) {
Source: &s.Metadata,
Distro: &d,
}
pres := NewPresenter(matches, pkg.FromCatalog(catalog), ctx, models.NewMetadataMock(), nil, nil)
pres := NewPresenter(matches, nil, pkg.FromCatalog(catalog), ctx, models.NewMetadataMock(), nil, nil)
// TODO: add a constructor for a match.Match when the data is better shaped
@ -355,7 +355,7 @@ func TestEmptyJsonPresenter(t *testing.T) {
Distro: &d,
}
pres := NewPresenter(matches, []pkg.Package{}, ctx, nil, nil, nil)
pres := NewPresenter(matches, nil, []pkg.Package{}, ctx, nil, nil, nil)
// run presenter
if err = pres.Present(&buffer); err != nil {

View file

@ -12,15 +12,15 @@ import (
// Document represents the JSON document to be presented
type Document struct {
Matches []Match `json:"matches"`
Source *source `json:"source"`
Distro distribution `json:"distro"`
Descriptor descriptor `json:"descriptor"`
Matches []Match `json:"matches"`
IgnoredMatches []IgnoredMatch `json:"ignoredMatches,omitempty"`
Source *source `json:"source"`
Distro distribution `json:"distro"`
Descriptor descriptor `json:"descriptor"`
}
// NewDocument creates and populates a new Document struct, representing the populated JSON document.
func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matches,
metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) (Document, error) {
func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) (Document, error) {
// we must preallocate the findings to ensure the JSON document does not show "null" when no matches are found
var findings = make([]Match, 0)
for _, m := range matches.Sorted() {
@ -46,10 +46,30 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc
src = &theSrc
}
var ignoredMatchModels []IgnoredMatch
for _, m := range ignoredMatches {
p := pkg.ByID(m.Package.ID, packages)
if p == nil {
return Document{}, fmt.Errorf("unable to find package in collection: %+v", p)
}
matchModel, err := newMatch(m.Match, *p, metadataProvider)
if err != nil {
return Document{}, err
}
ignoredMatch := IgnoredMatch{
Match: *matchModel,
AppliedIgnoreRules: mapIgnoreRules(m.AppliedIgnoreRules),
}
ignoredMatchModels = append(ignoredMatchModels, ignoredMatch)
}
return Document{
Matches: findings,
Source: src,
Distro: newDistribution(context.Distro),
Matches: findings,
IgnoredMatches: ignoredMatchModels,
Source: src,
Distro: newDistribution(context.Distro),
Descriptor: descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,

View file

@ -67,7 +67,7 @@ func TestPackagesAreSorted(t *testing.T) {
},
Distro: &d,
}
doc, err := NewDocument(packages, ctx, matches, NewMetadataMock(), nil, nil)
doc, err := NewDocument(packages, ctx, matches, nil, NewMetadataMock(), nil, nil)
if err != nil {
t.Fatalf("unable to get document: %+v", err)
}

View file

@ -0,0 +1,49 @@
package models
import "github.com/anchore/grype/grype/match"
type IgnoredMatch struct {
Match
AppliedIgnoreRules []IgnoreRule `json:"appliedIgnoreRules"`
}
type IgnoreRule struct {
Vulnerability string `json:"vulnerability,omitempty"`
Package *IgnoreRulePackage `json:"package,omitempty"`
}
type IgnoreRulePackage struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Type string `json:"type,omitempty"`
Location string `json:"location,omitempty"`
}
func newIgnoreRule(r match.IgnoreRule) IgnoreRule {
var ignoreRulePackage *IgnoreRulePackage
// We'll only set the package part of the rule not to `nil` if there are any values to fill out.
if p := r.Package; p.Name != "" || p.Version != "" || p.Type != "" || p.Location != "" {
ignoreRulePackage = &IgnoreRulePackage{
Name: r.Package.Name,
Version: r.Package.Version,
Type: r.Package.Type,
Location: r.Package.Location,
}
}
return IgnoreRule{
Vulnerability: r.Vulnerability,
Package: ignoreRulePackage,
}
}
func mapIgnoreRules(rules []match.IgnoreRule) []IgnoreRule {
var result []IgnoreRule
for _, rule := range rules {
result = append(result, newIgnoreRule(rule))
}
return result
}

View file

@ -0,0 +1,97 @@
package models
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/anchore/grype/grype/match"
)
func TestNewIgnoreRule(t *testing.T) {
cases := []struct {
name string
input match.IgnoreRule
expected IgnoreRule
}{
{
name: "no values",
input: match.IgnoreRule{},
expected: IgnoreRule{
Vulnerability: "",
Package: nil,
},
},
{
name: "only vulnerability field",
input: match.IgnoreRule{
Vulnerability: "CVE-2020-1234",
},
expected: IgnoreRule{
Vulnerability: "CVE-2020-1234",
},
},
{
name: "all package fields",
input: match.IgnoreRule{
Package: match.IgnoreRulePackage{
Name: "libc",
Version: "3.0.0",
Type: "rpm",
Location: "/some/location",
},
},
expected: IgnoreRule{
Package: &IgnoreRulePackage{
Name: "libc",
Version: "3.0.0",
Type: "rpm",
Location: "/some/location",
},
},
},
{
name: "only one package field",
input: match.IgnoreRule{
Package: match.IgnoreRulePackage{
Type: "apk",
},
},
expected: IgnoreRule{
Package: &IgnoreRulePackage{
Type: "apk",
},
},
},
{
name: "all fields",
input: match.IgnoreRule{
Vulnerability: "CVE-2020-1234",
Package: match.IgnoreRulePackage{
Name: "libc",
Version: "3.0.0",
Type: "rpm",
Location: "/some/location",
},
},
expected: IgnoreRule{
Vulnerability: "CVE-2020-1234",
Package: &IgnoreRulePackage{
Name: "libc",
Version: "3.0.0",
Type: "rpm",
Location: "/some/location",
},
},
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
actual := newIgnoreRule(testCase.input)
if diff := cmp.Diff(testCase.expected, actual); diff != "" {
t.Errorf("(-expected +actual):\n%s", diff)
}
})
}
}

View file

@ -19,17 +19,16 @@ type Presenter interface {
}
// GetPresenter retrieves a Presenter that matches a CLI option
func GetPresenter(presenterConfig Config, matches match.Matches, packages []pkg.Package, context pkg.Context,
metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) Presenter {
func GetPresenter(presenterConfig Config, matches match.Matches, ignoredMatches []match.IgnoredMatch, packages []pkg.Package, context pkg.Context, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}) Presenter {
switch presenterConfig.format {
case jsonFormat:
return json.NewPresenter(matches, packages, context, metadataProvider, appConfig, dbStatus)
return json.NewPresenter(matches, ignoredMatches, packages, context, metadataProvider, appConfig, dbStatus)
case tableFormat:
return table.NewPresenter(matches, packages, metadataProvider)
case cycloneDXFormat:
return cyclonedx.NewPresenter(matches, packages, context.Source, metadataProvider)
case templateFormat:
return template.NewPresenter(matches, packages, context, metadataProvider, appConfig, dbStatus, presenterConfig.templateFilePath)
return template.NewPresenter(matches, ignoredMatches, packages, context, metadataProvider, appConfig, dbStatus, presenterConfig.templateFilePath)
default:
return nil
}

View file

@ -19,6 +19,7 @@ import (
// Presenter is an implementation of presenter.Presenter that formats output according to a user-provided Go text template.
type Presenter struct {
matches match.Matches
ignoredMatches []match.IgnoredMatch
packages []pkg.Package
context pkg.Context
metadataProvider vulnerability.MetadataProvider
@ -28,9 +29,10 @@ type Presenter struct {
}
// NewPresenter returns a new template.Presenter.
func NewPresenter(matches match.Matches, packages []pkg.Package, context pkg.Context, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}, pathToTemplateFile string) *Presenter {
func NewPresenter(matches match.Matches, ignoredMatches []match.IgnoredMatch, packages []pkg.Package, context pkg.Context, metadataProvider vulnerability.MetadataProvider, appConfig interface{}, dbStatus interface{}, pathToTemplateFile string) *Presenter {
return &Presenter{
matches: matches,
ignoredMatches: ignoredMatches,
packages: packages,
metadataProvider: metadataProvider,
context: context,
@ -58,7 +60,8 @@ func (pres *Presenter) Present(output io.Writer) error {
return fmt.Errorf("unable to parse template: %w", err)
}
document, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.metadataProvider, pres.appConfig, pres.dbStatus)
document, err := models.NewDocument(pres.packages, pres.context, pres.matches, pres.ignoredMatches, pres.metadataProvider,
pres.appConfig, pres.dbStatus)
if err != nil {
return err
}

View file

@ -23,7 +23,7 @@ func TestPresenter_Present(t *testing.T) {
}
templateFilePath := path.Join(workingDirectory, "./test-fixtures/test.template")
templatePresenter := NewPresenter(matches, packages, context, metadataProvider, appConfig, dbStatus, templateFilePath)
templatePresenter := NewPresenter(matches, nil, packages, context, metadataProvider, appConfig, dbStatus, templateFilePath)
var buffer bytes.Buffer
if err := templatePresenter.Present(&buffer); err != nil {

View file

@ -7,6 +7,7 @@ import (
"github.com/adrg/xdg"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal"
"github.com/anchore/syft/syft/source"
@ -35,6 +36,7 @@ type Application struct {
FailOn string `mapstructure:"fail-on-severity"`
FailOnSeverity *vulnerability.Severity `json:"-"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
}
type Logging struct {