From fca4778e159c62f0eb471506d9f598257e22e859 Mon Sep 17 00:00:00 2001 From: James Hebden Date: Fri, 1 Sep 2023 12:23:59 +1000 Subject: [PATCH 1/3] Add --ignore-states flag for ignoring findings with by fix state Signed-off-by: James Hebden --- README.md | 2 +- cmd/grype/cli/commands/root.go | 9 +++++++++ cmd/grype/cli/options/grype.go | 6 ++++++ grype/vulnerability/fix.go | 9 +++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 22d0f65fdda..c3f6c99015d 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ NAME INSTALLED FIXED-IN VULNERABILITY SEVERITY 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.) +If you want Grype to only report vulnerabilities **that do not have a confirmed fix**, you can use the `--only-notfixed` flag. Alternatively, you can use the `--ignore-states` flag to filter results for vulnerabilities with specific states such as `wont-fix` (see `--help` for a list of valid fix states). These flags automatically add [ignore rules](#specifying-matches-to-ignore) into Grype's configuration, such that vulnerabilities which are fixed, or will not be fixed, will be ignored. ## VEX Support diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 4bc4552d3b7..1bc440be5d1 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -155,6 +155,15 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs opts.Ignore = append(opts.Ignore, ignoreFixedMatches...) } + for _, ignoreState := range opts.IgnoreStates { + switch grypeDb.FixState(ignoreState) { + case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState: + opts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState}) + default: + log.Warnf("ignoring unknown fix state %s for --ignore-states", ignoreState) + } + } + if err = applyVexRules(opts); err != nil { return fmt.Errorf("applying vex rules: %w", err) } diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go index d58d6be55ec..d75c0935c79 100644 --- a/cmd/grype/cli/options/grype.go +++ b/cmd/grype/cli/options/grype.go @@ -19,6 +19,7 @@ type Grype struct { CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix + IgnoreStates []string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these fix states Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image Search search `yaml:"search" json:"search" mapstructure:"search"` Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"` @@ -103,6 +104,11 @@ func (o *Grype) AddFlags(flags clio.FlagSet) { "ignore matches for vulnerabilities that are fixed", ) + flags.StringArrayVarP(&o.IgnoreStates, + "ignore-states", "", + fmt.Sprintf("ignore matches for vulnerabilities with specified fix states, options=%v", vulnerability.AllFixStates()), + ) + flags.BoolVarP(&o.ByCVE, "by-cve", "", "orient results by CVE instead of the original vulnerability ID when possible", diff --git a/grype/vulnerability/fix.go b/grype/vulnerability/fix.go index a8d88a52cf4..3cb81469aae 100644 --- a/grype/vulnerability/fix.go +++ b/grype/vulnerability/fix.go @@ -4,6 +4,15 @@ import ( grypeDb "github.com/anchore/grype/grype/db/v5" ) +func AllFixStates() []grypeDb.FixState { + return []grypeDb.FixState{ + grypeDb.FixedState, + grypeDb.NotFixedState, + grypeDb.UnknownFixState, + grypeDb.WontFixState, + } +} + type Fix struct { Versions []string State grypeDb.FixState From d20c4dc1b697fea4293090f3e0f0136d589d70c8 Mon Sep 17 00:00:00 2001 From: James Hebden Date: Wed, 11 Oct 2023 16:15:11 +1100 Subject: [PATCH 2/3] ignore options checked before scan, fail on invalid ignore states, ignore states comma-separated Signed-off-by: James Hebden --- cmd/grype/cli/commands/root.go | 34 +++++++++++----------- cmd/grype/cli/options/grype.go | 6 ++-- internal/stringutil/string_helpers.go | 11 +++++++ internal/stringutil/string_helpers_test.go | 34 ++++++++++++++++++++++ 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 1bc440be5d1..edc4fb7a584 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -115,6 +115,23 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs var s *sbom.SBOM var pkgContext pkg.Context + if opts.OnlyFixed { + opts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...) + } + + if opts.OnlyNotFixed { + opts.Ignore = append(opts.Ignore, ignoreFixedMatches...) + } + + for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) { + switch grypeDb.FixState(ignoreState) { + case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState: + opts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState}) + default: + return fmt.Errorf("unknown fix state %s was supplied for --ignore-states", ignoreState) + } + } + err = parallel( func() error { checkForAppUpdate(app.ID(), opts) @@ -147,23 +164,6 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs defer dbCloser.Close() } - if opts.OnlyFixed { - opts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...) - } - - if opts.OnlyNotFixed { - opts.Ignore = append(opts.Ignore, ignoreFixedMatches...) - } - - for _, ignoreState := range opts.IgnoreStates { - switch grypeDb.FixState(ignoreState) { - case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState: - opts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState}) - default: - log.Warnf("ignoring unknown fix state %s for --ignore-states", ignoreState) - } - } - if err = applyVexRules(opts); err != nil { return fmt.Errorf("applying vex rules: %w", err) } diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go index d75c0935c79..8219d77c369 100644 --- a/cmd/grype/cli/options/grype.go +++ b/cmd/grype/cli/options/grype.go @@ -19,7 +19,7 @@ type Grype struct { CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix - IgnoreStates []string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these fix states + IgnoreStates string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these comma-separated fix states Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image Search search `yaml:"search" json:"search" mapstructure:"search"` Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"` @@ -104,9 +104,9 @@ func (o *Grype) AddFlags(flags clio.FlagSet) { "ignore matches for vulnerabilities that are fixed", ) - flags.StringArrayVarP(&o.IgnoreStates, + flags.StringVarP(&o.IgnoreStates, "ignore-states", "", - fmt.Sprintf("ignore matches for vulnerabilities with specified fix states, options=%v", vulnerability.AllFixStates()), + fmt.Sprintf("ignore matches for vulnerabilities with specified comma separated fix states, options=%v", vulnerability.AllFixStates()), ) flags.BoolVarP(&o.ByCVE, diff --git a/internal/stringutil/string_helpers.go b/internal/stringutil/string_helpers.go index 1ff56e35c54..25d21f02c5c 100644 --- a/internal/stringutil/string_helpers.go +++ b/internal/stringutil/string_helpers.go @@ -23,3 +23,14 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool { return false } + +// SplitCommaSeparatedString returns a slice of strings separated from the input string by commas +func SplitCommaSeparatedString(input string) []string { + output := make([]string, 0) + for _, inputItem := range strings.Split(input, ",") { + if len(inputItem) > 0 { + output = append(output, inputItem) + } + } + return output +} diff --git a/internal/stringutil/string_helpers_test.go b/internal/stringutil/string_helpers_test.go index b5171686801..89baa28f9b1 100644 --- a/internal/stringutil/string_helpers_test.go +++ b/internal/stringutil/string_helpers_test.go @@ -120,3 +120,37 @@ func TestHasAnyOfPrefixes(t *testing.T) { }) } } + +func TestSplitCommaSeparatedString(t *testing.T) { + tests := []struct { + input string + expected []string + }{ + { + input: "testing", + expected: []string{"testing"}, + }, + { + input: "", + expected: []string{}, + }, + { + input: "testing1,testing2", + expected: []string{"testing1", "testing2"}, + }, + { + input: "testing1,,testing2,testing3", + expected: []string{"testing1", "testing2", "testing3"}, + }, + { + input: "testing1,testing2,,", + expected: []string{"testing1", "testing2"}, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + assert.Equal(t, test.expected, SplitCommaSeparatedString(test.input)) + }) + } +} From 9c6dc0b36d1c52aa84de81f6af4c139b084f32b9 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 17 Oct 2023 11:25:34 -0400 Subject: [PATCH 3/3] Add CLI tests for new --ignore-states flag Signed-off-by: Will Murphy --- test/cli/cmd_test.go | 26 ++++++++++++++++++++++++++ test/cli/trait_assertions_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/test/cli/cmd_test.go b/test/cli/cmd_test.go index fc341dda90d..692d144fc57 100644 --- a/test/cli/cmd_test.go +++ b/test/cli/cmd_test.go @@ -65,6 +65,32 @@ func TestCmd(t *testing.T) { assertFailingReturnCode, }, }, + { + name: "ignore-states wired up", + args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "unknown"}, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High"}), + assertNotInOutput("CVE-2014-6052"), + }, + }, + { + name: "ignore-states wired up - ignore fixed", + args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "fixed"}, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertRowInStdOut([]string{"libvncserver", "0.9.9", "apk", "CVE-2014-6052", "High"}), + assertNotInOutput("GHSA-pq64-v7f5-gqh8"), + }, + }, + { + name: "ignore-states wired up - ignore fixed, show suppressed", + args: []string{"./test-fixtures/sbom-grype-source.json", "--ignore-states", "fixed", "--show-suppressed"}, + assertions: []traitAssertion{ + assertSucceedingReturnCode, + assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High", "(suppressed)"}), + }, + }, } for _, test := range tests { diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index fa89d35ea08..9681c91e154 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -32,3 +32,34 @@ func assertSucceedingReturnCode(tb testing.TB, _, _ string, rc int) { tb.Errorf("expected to succeed but got rc=%d", rc) } } + +func assertRowInStdOut(row []string) traitAssertion { + return func(tb testing.TB, stdout, stderr string, _ int) { + tb.Helper() + + for _, line := range strings.Split(stdout, "\n") { + lineMatched := false + for _, column := range row { + if !strings.Contains(line, column) { + // it wasn't this line + lineMatched = false + break + } + lineMatched = true + } + if lineMatched { + return + } + } + // none of the lines matched + tb.Errorf("expected stdout to contain %s, but it did not", strings.Join(row, " ")) + } +} + +func assertNotInOutput(notWanted string) traitAssertion { + return func(tb testing.TB, stdout, stderr string, _ int) { + if strings.Contains(stdout, notWanted) { + tb.Errorf("got unwanted %s in stdout %s", notWanted, stdout) + } + } +}