mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
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:
parent
e6831d9444
commit
f86fd7eb38
15 changed files with 750 additions and 30 deletions
64
README.md
64
README.md
|
@ -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:
|
||||
|
||||
|
|
11
cmd/root.go
11
cmd/root.go
|
@ -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
2
go.mod
|
@ -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
167
grype/match/ignore.go
Normal 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
318
grype/match/ignore_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -13,14 +13,14 @@ import (
|
|||
// Document represents the JSON document to be presented
|
||||
type Document struct {
|
||||
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,8 +46,28 @@ 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,
|
||||
IgnoredMatches: ignoredMatchModels,
|
||||
Source: src,
|
||||
Distro: newDistribution(context.Distro),
|
||||
Descriptor: descriptor{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
49
grype/presenter/models/ignore.go
Normal file
49
grype/presenter/models/ignore.go
Normal 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
|
||||
}
|
97
grype/presenter/models/ignore_test.go
Normal file
97
grype/presenter/models/ignore_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue