From a80607b7e517d61cc13f11495dec3653efc89fc9 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:59:12 +1100 Subject: [PATCH 01/25] ci: configure exhaustive linter It's either make it more sane or turn it off. --- .golangci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index cda0382b..7668f3b2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,6 +39,7 @@ linters: - revive - tagalign - whitespace + - musttag # deprecated linters - interfacer - golint @@ -62,5 +63,11 @@ linters-settings: audit: disabled G101: # "Look for hard-coded credentials" mode: strict + cyclop: max-complexity: 20 + + exhaustive: + # Presence of "default" case in switch statements satisfies exhaustiveness, + # even if all enum members are not listed. + default-signifies-exhaustive: true From d0f2fc142b4515c91c94c2f26deff31d5682a16d Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:14:19 +1100 Subject: [PATCH 02/25] feat: add ability to parse an ignore file --- src/findingconfig/ignores.go | 88 +++++++++++++++++++++++++++++++ src/findingconfig/ignores_test.go | 79 +++++++++++++++++++++++++++ src/findingconfig/until.go | 50 ++++++++++++++++++ src/findingconfig/until_test.go | 30 +++++++++++ src/go.mod | 2 +- 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/findingconfig/ignores.go create mode 100644 src/findingconfig/ignores_test.go create mode 100644 src/findingconfig/until.go create mode 100644 src/findingconfig/until_test.go diff --git a/src/findingconfig/ignores.go b/src/findingconfig/ignores.go new file mode 100644 index 00000000..94140c59 --- /dev/null +++ b/src/findingconfig/ignores.go @@ -0,0 +1,88 @@ +package findingconfig + +import ( + "bytes" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// detailedIgnore represents the detailed encoding of Ignore, used +// for deserialization. Fields must match Ignore. +type detailedIgnore struct { + ID string `yaml:"id"` + Until UntilTime + Reason string +} + +// An entry in an ignore file used by the plugin. +type Ignore struct { + ID string + Until UntilTime + Reason string +} + +// UnmarshalYAML is a custom YAML unmarshaller that supports a simple string +// encoding and full encoding that specifies all fields. The simple encoding is +// the string ID, the complex version allows the full ID, Until and Reason +// triple. +func (f *Ignore) UnmarshalYAML(value *yaml.Node) error { + var deserialized Ignore + switch value.Kind { + case yaml.ScalarNode: + // simple string value, interpret as ID + deserialized.ID = value.Value + + case yaml.MappingNode: + // interpret mapping as the full version with all fields + var fields detailedIgnore + err := value.Decode(&fields) + if err != nil { + return err + } + + deserialized = Ignore(fields) + + default: + return fmt.Errorf("unknown type for ignore entry (%d) at line %d:%d", value.Kind, value.Line, value.Column) + } + + *f = deserialized + + return nil +} + +type Ignores struct { + Ignores []Ignore +} + +func LoadIgnores(filename string) ([]Ignore, error) { + content, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var i Ignores + err = unmarshalYAML(content, &i) + if err != nil { + return nil, err + } + + return i.Ignores, nil +} + +// unmarshalYAML decodes a YAML encoded byte stream into the supplied pointer +// field, returning an error if decoding fails. Unknown keys in the source YAML +// will cause unmarshalling to fail. We use more strict parsing to help make +// configuration errors more visible to the users of the plugin. +func unmarshalYAML(in []byte, out any) error { + r := bytes.NewReader(in) + + dec := yaml.NewDecoder(r) + dec.KnownFields(true) + + err := dec.Decode(out) + + return err +} diff --git a/src/findingconfig/ignores_test.go b/src/findingconfig/ignores_test.go new file mode 100644 index 00000000..ff225ff7 --- /dev/null +++ b/src/findingconfig/ignores_test.go @@ -0,0 +1,79 @@ +package findingconfig_test + +import ( + "fmt" + "os" + "testing" + + "github.com/cultureamp/ecrscanresults/findingconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadIgnores_Succeeds(t *testing.T) { + in := []byte(` +ignores: + - id: CVE-2023-1234 + until: 2015-02-15 + reason: We don't talk about CVE-2023-1234 + - CVE-2023-9876 +`) + f, err := os.CreateTemp(t.TempDir(), "ignores*.yaml") + require.NoError(t, err) + err = os.WriteFile(f.Name(), in, 0600) + require.NoError(t, err) + + i, err := findingconfig.LoadIgnores(f.Name()) + require.NoError(t, err) + + assert.Equal(t, []findingconfig.Ignore{ + {ID: "CVE-2023-1234", Until: findingconfig.MustParseUntil("2015-02-15"), Reason: "We don't talk about CVE-2023-1234"}, + {ID: "CVE-2023-9876", Until: findingconfig.UntilTime{}, Reason: ""}, + }, i) +} + +func TestLoadIgnores_Fails(t *testing.T) { + + cases := []struct { + in string + expectedError string + }{ + { + in: ` +ignores: + - ["nested array"] +`, + expectedError: "unknown type for ignore entry", + }, + { + in: ` +ignor: +`, + expectedError: "field ignor not found in type findingconfig.Ignores", + }, + { + in: ` +ignores: + - id: CVE-123 + until: 15-Jan-05 +`, + expectedError: "did not match the expected YYYY-MM-dd format", + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { + in := []byte(c.in) + + f, err := os.CreateTemp(t.TempDir(), "ignores*.yaml") + require.NoError(t, err) + + err = os.WriteFile(f.Name(), in, 0600) + require.NoError(t, err) + + _, err = findingconfig.LoadIgnores(f.Name()) + require.ErrorContains(t, err, c.expectedError) + }) + } + +} diff --git a/src/findingconfig/until.go b/src/findingconfig/until.go new file mode 100644 index 00000000..68165cd6 --- /dev/null +++ b/src/findingconfig/until.go @@ -0,0 +1,50 @@ +package findingconfig + +import ( + "errors" + "fmt" + "time" + + "gopkg.in/yaml.v3" +) + +const untilFormat = "2006-01-02" + +type UntilTime time.Time + +func (u *UntilTime) UnmarshalYAML(value *yaml.Node) error { + if value.Kind != yaml.ScalarNode { + return errors.New("unsupported type for Until value") + } + + t, err := ParseUntil(value.Value) + if err != nil { + return err + } + + *u = t + + return nil +} + +func (u UntilTime) String() string { + return time.Time(u).Format(untilFormat) +} + +func ParseUntil(dt string) (UntilTime, error) { + tm, err := time.Parse(untilFormat, dt) + if err != nil { + return UntilTime{}, fmt.Errorf("supplied until value '%s' did not match the expected YYYY-MM-dd format: %w", dt, err) + } + + return UntilTime(tm), nil +} + +func MustParseUntil(dt string) UntilTime { + u, err := ParseUntil(dt) + if err != nil { + panic(err) + } + + return u +} diff --git a/src/findingconfig/until_test.go b/src/findingconfig/until_test.go new file mode 100644 index 00000000..bf5fb1ae --- /dev/null +++ b/src/findingconfig/until_test.go @@ -0,0 +1,30 @@ +package findingconfig_test + +import ( + "testing" + "time" + + "github.com/cultureamp/ecrscanresults/findingconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestParseUntil(t *testing.T) { + type timer struct { + Until findingconfig.UntilTime + } + + in := ` +until: 2015-02-15 +` + + var out timer + err := yaml.Unmarshal([]byte(in), &out) + require.NoError(t, err) + + expected, _ := time.Parse("2006-01-02", "2015-02-15") + + assert.Equal(t, timer{findingconfig.UntilTime(expected)}, out) + +} diff --git a/src/go.mod b/src/go.mod index 792470c5..feed87cc 100644 --- a/src/go.mod +++ b/src/go.mod @@ -45,5 +45,5 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.14.0 golang.org/x/text v0.12.0 - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 ) From 23f4a929a7f9c0f6e7ffe29364230c8900565dbc Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:03:53 +1100 Subject: [PATCH 03/25] feat: load a series of ignore files from disk Load each then de-deplicate, allowing later definitions to override earlier ones. --- src/findingconfig/ignores.go | 49 ++++++++++++++++++ src/findingconfig/ignores_test.go | 83 +++++++++++++++++++++++++------ 2 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/findingconfig/ignores.go b/src/findingconfig/ignores.go index 94140c59..be2ec481 100644 --- a/src/findingconfig/ignores.go +++ b/src/findingconfig/ignores.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "os" + "slices" + "strings" "gopkg.in/yaml.v3" ) @@ -57,6 +59,49 @@ type Ignores struct { Ignores []Ignore } +// LoadExistingIgnores uses LoadIgnores to read any of the given set of files +// that exist. Repeated definitions for the same "Name" will overwrite each +// other. The later definition will take precedence. +func LoadExistingIgnores(filenames []string) ([]Ignore, error) { + ignores := []Ignore{} + + for _, name := range filenames { + if strings.HasPrefix(name, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("user home directory not available: %w", err) + } + name = home + name[1:] + } + + if _, err := os.Stat(name); err != nil { + continue + } + + partial, err := LoadIgnores(name) + if err != nil { + return nil, fmt.Errorf("loading ignore file '%s' failed: %w", name, err) + } + + ignores = append(ignores, partial...) + } + + // reverse the loaded ignores so that those loaded last take precedence in the + // following operations. + slices.Reverse(ignores) + + // sort keeping duplicates in order they were loaded + slices.SortStableFunc(ignores, compareIgnoreByID) + + // remove duplicates, keeping the first one + ignores = slices.CompactFunc(ignores, func(a, b Ignore) bool { + return compareIgnoreByID(a, b) == 0 + }) + + return ignores, nil +} + +// LoadIgnores parses a YAML ignore file from the given location. func LoadIgnores(filename string) ([]Ignore, error) { content, err := os.ReadFile(filename) if err != nil { @@ -86,3 +131,7 @@ func unmarshalYAML(in []byte, out any) error { return err } + +func compareIgnoreByID(a, b Ignore) int { + return strings.Compare(a.ID, b.ID) +} diff --git a/src/findingconfig/ignores_test.go b/src/findingconfig/ignores_test.go index ff225ff7..f01caeb8 100644 --- a/src/findingconfig/ignores_test.go +++ b/src/findingconfig/ignores_test.go @@ -11,19 +11,16 @@ import ( ) func TestLoadIgnores_Succeeds(t *testing.T) { - in := []byte(` + in := ` ignores: - id: CVE-2023-1234 until: 2015-02-15 reason: We don't talk about CVE-2023-1234 - CVE-2023-9876 -`) - f, err := os.CreateTemp(t.TempDir(), "ignores*.yaml") - require.NoError(t, err) - err = os.WriteFile(f.Name(), in, 0600) - require.NoError(t, err) +` - i, err := findingconfig.LoadIgnores(f.Name()) + f := createIgnoreFile(t, in) + i, err := findingconfig.LoadIgnores(f) require.NoError(t, err) assert.Equal(t, []findingconfig.Ignore{ @@ -63,17 +60,73 @@ ignores: for i, c := range cases { t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { - in := []byte(c.in) - - f, err := os.CreateTemp(t.TempDir(), "ignores*.yaml") - require.NoError(t, err) - - err = os.WriteFile(f.Name(), in, 0600) - require.NoError(t, err) + f := createIgnoreFile(t, c.in) - _, err = findingconfig.LoadIgnores(f.Name()) + _, err := findingconfig.LoadIgnores(f) require.ErrorContains(t, err, c.expectedError) }) } +} + +func TestLoadExistingIgnores(t *testing.T) { + contents := []string{ + ` +ignores: +`, + "skip", + ` +ignores: ~`, + ` +ignores: + - first-issue + - id: second-issue + reason: second issue earliest definition +`, + ` +ignores: +- id: second-issue + reason: second issue this reason should override earlier ones +- third-issue +`, + } + + files := createIgnoreFiles(t, contents) + + actual, err := findingconfig.LoadExistingIgnores(files) + require.NoError(t, err) + + assert.Equal(t, []findingconfig.Ignore{ + {ID: "first-issue", Until: findingconfig.UntilTime{}, Reason: ""}, + {ID: "second-issue", Until: findingconfig.UntilTime{}, Reason: "second issue this reason should override earlier ones"}, + {ID: "third-issue", Until: findingconfig.UntilTime{}, Reason: ""}, + }, actual) +} + +func createIgnoreFiles(t *testing.T, contents []string) []string { + t.Helper() + + files := make([]string, 0, len(contents)) + for _, c := range contents { + nm := "./file-does-not-exist.yaml" + + if c != "skip" { + nm = createIgnoreFile(t, c) + } + + files = append(files, nm) + } + + return files +} + +func createIgnoreFile(t *testing.T, contents string) string { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "ignores*.yaml") + require.NoError(t, err) + + err = os.WriteFile(f.Name(), []byte(contents), 0600) + require.NoError(t, err) + return f.Name() } From 2f85239ba5886773cefc01db1f1b8af09147a969 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:22:04 +1100 Subject: [PATCH 04/25] feat: ignored findings affect threshold calculations Ignore configuration is read from a series of possible locations, and the downloaded findings are summarized based on the supplied configuration. --- src/finding/summary.go | 61 ++++++++++++++++++++++++ src/finding/summary_test.go | 92 +++++++++++++++++++++++++++++++++++++ src/main.go | 26 ++++++++++- src/report/annotation.go | 2 + 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/finding/summary.go create mode 100644 src/finding/summary_test.go diff --git a/src/finding/summary.go b/src/finding/summary.go new file mode 100644 index 00000000..10116f10 --- /dev/null +++ b/src/finding/summary.go @@ -0,0 +1,61 @@ +package finding + +import ( + "slices" + + "github.com/aws/aws-sdk-go-v2/service/ecr/types" + "github.com/cultureamp/ecrscanresults/findingconfig" +) + +type SeverityCount struct { + // Included is the number of findings that count towards the threshold for this severity. + Included int32 + + // Ignored is the number of findings that were ignored for the purposes of the threshold. + Ignored int32 +} + +type Summary struct { + // the counts by threshold, taking ignore configuration into account + Counts map[types.FindingSeverity]SeverityCount + + // the set of finding IDs that have been ignored by configuration + Ignored map[string]struct{} +} + +func NewSummary() Summary { + return Summary{ + Counts: map[types.FindingSeverity]SeverityCount{ + "CRITICAL": {}, + "HIGH": {}, + }, + Ignored: map[string]struct{}{}, + } +} + +func Summarize(findings *types.ImageScanFindings, ignoreConfig []findingconfig.Ignore) Summary { + + summary := NewSummary() + + for _, f := range findings.Findings { + ignored := slices.ContainsFunc(ignoreConfig, func(i findingconfig.Ignore) bool { + return i.ID == *f.Name + }) + + counts := SeverityCount{} + if c, exists := summary.Counts[f.Severity]; exists { + counts = c + } + + if ignored { + summary.Ignored[*f.Name] = struct{}{} + counts.Ignored++ + } else { + counts.Included++ + } + + summary.Counts[f.Severity] = counts + } + + return summary +} diff --git a/src/finding/summary_test.go b/src/finding/summary_test.go new file mode 100644 index 00000000..69fa9179 --- /dev/null +++ b/src/finding/summary_test.go @@ -0,0 +1,92 @@ +package finding_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ecr/types" + "github.com/cultureamp/ecrscanresults/finding" + "github.com/cultureamp/ecrscanresults/findingconfig" + "github.com/hexops/autogold/v2" +) + +func TestSummarize(t *testing.T) { + cases := []struct { + name string + ignores []findingconfig.Ignore + data types.ImageScanFindings + expected autogold.Value + }{ + { + name: "no vulnerabilities", + data: types.ImageScanFindings{}, + expected: autogold.Expect(finding.Summary{ + Counts: map[types.FindingSeverity]finding.SeverityCount{ + types.FindingSeverity("CRITICAL"): {}, + types.FindingSeverity("HIGH"): {}, + }, + Ignored: map[string]struct{}{}, + }), + }, + { + name: "findings with no ignores", + data: types.ImageScanFindings{ + Findings: []types.ImageScanFinding{ + f("CVE-2019-5188", "HIGH"), + f("CVE-2019-5200", "CRITICAL"), + f("CVE-2019-5189", "HIGH"), + }, + }, + expected: autogold.Expect(finding.Summary{ + Counts: map[types.FindingSeverity]finding.SeverityCount{ + types.FindingSeverity("CRITICAL"): {Included: 1}, + types.FindingSeverity("HIGH"): {Included: 2}, + }, + Ignored: map[string]struct{}{}, + }), + }, + { + name: "ignores affect counts", + data: types.ImageScanFindings{ + Findings: []types.ImageScanFinding{ + f("CVE-2019-5188", "HIGH"), + f("CVE-2019-5200", "CRITICAL"), + f("CVE-2019-5189", "HIGH"), + }, + }, + ignores: []findingconfig.Ignore{ + i("CVE-2019-5189"), // part of the summary + i("CVE-2019-6000"), // not part of it + }, + expected: autogold.Expect(finding.Summary{ + Counts: map[types.FindingSeverity]finding.SeverityCount{ + types.FindingSeverity("CRITICAL"): {Included: 1}, + types.FindingSeverity("HIGH"): { + Included: 1, + Ignored: 1, + }, + }, + Ignored: map[string]struct{}{"CVE-2019-5189": {}}, + }), + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + summary := finding.Summarize(&c.data, c.ignores) + + c.expected.Equal(t, summary) + }) + } +} + +func f(name string, severity types.FindingSeverity) types.ImageScanFinding { + return types.ImageScanFinding{ + Name: &name, + Severity: severity, + } +} + +func i(id string) findingconfig.Ignore { + return findingconfig.Ignore{ID: id} +} diff --git a/src/main.go b/src/main.go index 5e35d303..65cf67e8 100644 --- a/src/main.go +++ b/src/main.go @@ -12,6 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/cultureamp/ecrscanresults/buildkite" + "github.com/cultureamp/ecrscanresults/finding" + "github.com/cultureamp/ecrscanresults/findingconfig" "github.com/cultureamp/ecrscanresults/registry" "github.com/cultureamp/ecrscanresults/report" "github.com/cultureamp/ecrscanresults/runtimeerrors" @@ -106,8 +108,27 @@ func runCommand(ctx context.Context, pluginConfig Config, agent buildkite.Agent) buildkite.Logf("retrieved. %d findings in report.\n", len(findings.ImageScanFindings.Findings)) - criticalFindings := findings.ImageScanFindings.FindingSeverityCounts["CRITICAL"] - highFindings := findings.ImageScanFindings.FindingSeverityCounts["HIGH"] + buildkite.Logf("loading finding ignore files ...\n") + + ignoreConfig, err := findingconfig.LoadExistingIgnores([]string{ + ".ecr-scan-results-ignore.yaml", + ".ecr-scan-results-ignore.yml", + ".buildkite/ecr-scan-results-ignore.yaml", + ".buildkite/ecr-scan-results-ignore.yml", + "buildkite/ecr-scan-results-ignore.yaml", + "buildkite/ecr-scan-results-ignore.yml", + "~/.ecr-scan-results-ignore.yaml", + "~/.ecr-scan-results-ignore.yml", + }) + if err != nil { + return runtimeerrors.NonFatal("could not load finding ignore configuration", err) + } + + // summarize findings, taking ignore configuration into account + findingSummary := finding.Summarize(findings.ImageScanFindings, ignoreConfig) + + criticalFindings := findingSummary.Counts["CRITICAL"].Included + highFindings := findingSummary.Counts["HIGH"].Included overThreshold := criticalFindings > pluginConfig.CriticalSeverityThreshold || highFindings > pluginConfig.HighSeverityThreshold @@ -119,6 +140,7 @@ func runCommand(ctx context.Context, pluginConfig Config, agent buildkite.Agent) Image: imageID, ImageLabel: pluginConfig.ImageLabel, ScanFindings: *findings.ImageScanFindings, + FindingSummary: findingSummary, CriticalSeverityThreshold: pluginConfig.CriticalSeverityThreshold, HighSeverityThreshold: pluginConfig.HighSeverityThreshold, } diff --git a/src/report/annotation.go b/src/report/annotation.go index 5ced1f9b..c9e0c411 100644 --- a/src/report/annotation.go +++ b/src/report/annotation.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr/types" + "github.com/cultureamp/ecrscanresults/finding" "github.com/cultureamp/ecrscanresults/registry" "github.com/justincampbell/timeago" "golang.org/x/exp/maps" @@ -25,6 +26,7 @@ type AnnotationContext struct { Image registry.RegistryInfo ImageLabel string ScanFindings types.ImageScanFindings + FindingSummary finding.Summary CriticalSeverityThreshold int32 HighSeverityThreshold int32 } From 178483250483e5d8fd69a7da4c7d60698d111d1c Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:42:27 +1100 Subject: [PATCH 05/25] fix: use summarized results in report annotation --- src/report/annotation.go | 10 +++++----- src/report/annotation.gohtml | 12 ++++++------ src/report/annotation_function_test.go | 22 ++++++++++++---------- src/report/annotation_test.go | 13 ++++++++----- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/report/annotation.go b/src/report/annotation.go index c9e0c411..7b14b2a2 100644 --- a/src/report/annotation.go +++ b/src/report/annotation.go @@ -94,7 +94,7 @@ func sortFindings(findings []types.ImageScanFinding) []types.ImageScanFinding { // sort by severity rank, then CVE _descending_ slices.SortFunc(sorted, func(a, b types.ImageScanFinding) int { - sevRank := compareSeverities(string(a.Severity), string(b.Severity)) + sevRank := compareSeverities(a.Severity, b.Severity) if sevRank != 0 { return sevRank } @@ -107,7 +107,7 @@ func sortFindings(findings []types.ImageScanFinding) []types.ImageScanFinding { return sorted } -func sortSeverities(severityCounts map[string]int32) []string { +func sortSeverities(severityCounts map[types.FindingSeverity]finding.SeverityCount) []types.FindingSeverity { // severities are the map key in the incoming data structure severities := maps.Keys(severityCounts) @@ -117,15 +117,15 @@ func sortSeverities(severityCounts map[string]int32) []string { } // sort severity strings by rank, then alphabetically -func compareSeverities(a, b string) int { - rank := rankSeverity(a) - rankSeverity(b) +func compareSeverities(a, b types.FindingSeverity) int { + rank := rankSeverity(string(a)) - rankSeverity(string(b)) if rank != 0 { return rank } // for unknown severities, sort alphabetically - return strings.Compare(a, b) + return strings.Compare(string(a), string(b)) } func rankSeverity(s string) int { diff --git a/src/report/annotation.gohtml b/src/report/annotation.gohtml index 7002a0b6..525f3c11 100644 --- a/src/report/annotation.gohtml +++ b/src/report/annotation.gohtml @@ -14,18 +14,18 @@ there is no indentation: indented output can be rendered differently. {{ else }}
{{ .Image.Name }}:{{ .Image.Tag }}
@@ -26,6 +27,7 @@ there is no indentation: indented output can be rendered differently.test-repo:digest-value
@@ -14,6 +15,7 @@test-repo:digest-value
diff --git a/src/report/testdata/TestReports/no_vulnerabilities.golden b/src/report/testdata/TestReports/no_vulnerabilities.golden index efdda81e..388e1d9b 100644 --- a/src/report/testdata/TestReports/no_vulnerabilities.golden +++ b/src/report/testdata/TestReports/no_vulnerabilities.golden @@ -2,6 +2,7 @@ +test-repo:digest-value
+ + +CVE | +Severity | +Effects | +CVSS score | +Vector | +
---|---|---|---|---|
CVE-2019-5200 | +Critical | +5200-package 5200-version | +10.0 | +AV:L/AC:L/Au:N/C:P/I:P/A:P | +
CVE-2019-5188 | +High | +e2fsprogs 1.44.1-1ubuntu1.1 | +4.6 | +AV:L/AC:L/Au:N/C:P/I:P/A:P | +
CVE-2019-5300 (ignored) | +Aa-Bogus-Severity | +5300-package 5300-version | +10.0 | +AV:L/AC:L/Au:N/C:P/I:P/A:P | +
+scan completed: | +source updated: +
+` From c26c6d54667a6a311b3faff1f5316fa0c0fe5404 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Tue, 21 Nov 2023 19:28:55 +1100 Subject: [PATCH 07/25] fix: transition to view objects for report Using the AWS structs directly was becoming cumbersome. This allows for a simpler template: adding ignore lists in the existing structure was becoming too complicated. --- src/finding/summary.go | 107 ++++++++-- src/finding/summary_test.go | 37 +++- src/main.go | 1 - src/report/annotation.go | 20 +- src/report/annotation.gohtml | 18 +- src/report/annotation_test.go | 199 +++++------------- .../TestReports/findings_included.golden | 6 - .../TestReports/some_findings_ignored.golden | 6 - 8 files changed, 192 insertions(+), 202 deletions(-) diff --git a/src/finding/summary.go b/src/finding/summary.go index 74a5deb7..9091beb0 100644 --- a/src/finding/summary.go +++ b/src/finding/summary.go @@ -2,11 +2,33 @@ package finding import ( "slices" + "time" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/cultureamp/ecrscanresults/findingconfig" ) +type Detail struct { + // The name associated with the finding, usually a CVE number. + Name string + + Uri string + + // The description of the finding. + Description string + + // The finding severity. + Severity types.FindingSeverity + + PackageName string + PackageVersion string + CVSS2Score string + CVSS2Vector string + + Ignore *findingconfig.Ignore +} + type SeverityCount struct { // Included is the number of findings that count towards the threshold for this severity. Included int32 @@ -19,13 +41,38 @@ type Summary struct { // the counts by threshold, taking ignore configuration into account Counts map[types.FindingSeverity]SeverityCount + Details []Detail + // the set of finding IDs that have been ignored by configuration - Ignored map[string]struct{} + Ignored []Detail + + // The time of the last completed image scan. + ImageScanCompletedAt *time.Time + + // The time when the vulnerability data was last scanned. + VulnerabilitySourceUpdatedAt *time.Time +} + +func (s *Summary) addDetail(d Detail) { + s.Details = append(s.Details, d) + s.updateCount(d.Severity, SeverityCount{Included: 1}) } -func (s Summary) IsIgnored(id string) bool { - _, found := s.Ignored[id] - return found +func (s *Summary) addIgnored(d Detail) { + s.Ignored = append(s.Ignored, d) + s.updateCount(d.Severity, SeverityCount{Ignored: 1}) +} + +func (s *Summary) updateCount(severity types.FindingSeverity, updateBy SeverityCount) { + counts := SeverityCount{} + if c, exists := s.Counts[severity]; exists { + counts = c + } + + counts.Ignored += updateBy.Ignored + counts.Included += updateBy.Included + + s.Counts[severity] = counts } func NewSummary() Summary { @@ -34,33 +81,59 @@ func NewSummary() Summary { "CRITICAL": {}, "HIGH": {}, }, - Ignored: map[string]struct{}{}, + Details: []Detail{}, + Ignored: []Detail{}, } } func Summarize(findings *types.ImageScanFindings, ignoreConfig []findingconfig.Ignore) Summary { - summary := NewSummary() + summary.ImageScanCompletedAt = findings.ImageScanCompletedAt + summary.VulnerabilitySourceUpdatedAt = findings.VulnerabilitySourceUpdatedAt + for _, f := range findings.Findings { - ignored := slices.ContainsFunc(ignoreConfig, func(i findingconfig.Ignore) bool { - return i.ID == *f.Name + var ignore *findingconfig.Ignore + detail := findingToDetail(f) + + index := slices.IndexFunc(ignoreConfig, func(ignore findingconfig.Ignore) bool { + return ignore.ID == detail.Name }) - counts := SeverityCount{} - if c, exists := summary.Counts[f.Severity]; exists { - counts = c + if index >= 0 { + ignore = &ignoreConfig[index] } - if ignored { - summary.Ignored[*f.Name] = struct{}{} - counts.Ignored++ + detail.Ignore = ignore + + if ignore == nil { + summary.addDetail(detail) } else { - counts.Included++ + summary.addIgnored(detail) } - - summary.Counts[f.Severity] = counts } return summary } + +func findingToDetail(finding types.ImageScanFinding) Detail { + return Detail{ + Name: aws.ToString(finding.Name), + Uri: aws.ToString(finding.Uri), + Description: aws.ToString(finding.Description), + Severity: finding.Severity, + PackageName: findingAttributeValue(finding, "package_name"), + PackageVersion: findingAttributeValue(finding, "package_version"), + CVSS2Score: findingAttributeValue(finding, "CVSS2_SCORE"), + CVSS2Vector: findingAttributeValue(finding, "CVSS2_VECTOR"), + } +} + +func findingAttributeValue(finding types.ImageScanFinding, name string) string { + for _, a := range finding.Attributes { + if aws.ToString(a.Key) == name { + return aws.ToString(a.Value) + } + } + return "" +} diff --git a/src/finding/summary_test.go b/src/finding/summary_test.go index 69fa9179..145195b4 100644 --- a/src/finding/summary_test.go +++ b/src/finding/summary_test.go @@ -24,7 +24,8 @@ func TestSummarize(t *testing.T) { types.FindingSeverity("CRITICAL"): {}, types.FindingSeverity("HIGH"): {}, }, - Ignored: map[string]struct{}{}, + Details: []finding.Detail{}, + Ignored: []finding.Detail{}, }), }, { @@ -41,7 +42,21 @@ func TestSummarize(t *testing.T) { types.FindingSeverity("CRITICAL"): {Included: 1}, types.FindingSeverity("HIGH"): {Included: 2}, }, - Ignored: map[string]struct{}{}, + Details: []finding.Detail{ + { + Name: "CVE-2019-5188", + Severity: types.FindingSeverity("HIGH"), + }, + { + Name: "CVE-2019-5200", + Severity: types.FindingSeverity("CRITICAL"), + }, + { + Name: "CVE-2019-5189", + Severity: types.FindingSeverity("HIGH"), + }, + }, + Ignored: []finding.Detail{}, }), }, { @@ -65,7 +80,23 @@ func TestSummarize(t *testing.T) { Ignored: 1, }, }, - Ignored: map[string]struct{}{"CVE-2019-5189": {}}, + Details: []finding.Detail{ + { + Name: "CVE-2019-5188", + Severity: types.FindingSeverity("HIGH"), + }, + { + Name: "CVE-2019-5200", + Severity: types.FindingSeverity("CRITICAL"), + }, + }, + Ignored: []finding.Detail{{ + Name: "CVE-2019-5189", + Severity: types.FindingSeverity("HIGH"), + Ignore: &findingconfig.Ignore{ + ID: "CVE-2019-5189", + }, + }}, }), }, } diff --git a/src/main.go b/src/main.go index 65cf67e8..0d8dcbd8 100644 --- a/src/main.go +++ b/src/main.go @@ -139,7 +139,6 @@ func runCommand(ctx context.Context, pluginConfig Config, agent buildkite.Agent) annotationCtx := report.AnnotationContext{ Image: imageID, ImageLabel: pluginConfig.ImageLabel, - ScanFindings: *findings.ImageScanFindings, FindingSummary: findingSummary, CriticalSeverityThreshold: pluginConfig.CriticalSeverityThreshold, HighSeverityThreshold: pluginConfig.HighSeverityThreshold, diff --git a/src/report/annotation.go b/src/report/annotation.go index 7b14b2a2..fd00c39a 100644 --- a/src/report/annotation.go +++ b/src/report/annotation.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/cultureamp/ecrscanresults/finding" "github.com/cultureamp/ecrscanresults/registry" @@ -25,7 +24,6 @@ var annotationTemplateSource string type AnnotationContext struct { Image registry.RegistryInfo ImageLabel string - ScanFindings types.ImageScanFindings FindingSummary finding.Summary CriticalSeverityThreshold int32 HighSeverityThreshold int32 @@ -39,8 +37,7 @@ func (c AnnotationContext) Render() ([]byte, error) { c := cases.Title(language.English) return c.String(s) }, - "lowerCase": strings.ToLower, - "findingAttribute": findingAttributeValue, + "lowerCase": strings.ToLower, "nbsp": func(input string) any { if len(input) > 0 { return input @@ -79,21 +76,12 @@ func (c AnnotationContext) Render() ([]byte, error) { return buf.Bytes(), nil } -func findingAttributeValue(name string, finding types.ImageScanFinding) string { - for _, a := range finding.Attributes { - if aws.ToString(a.Key) == name { - return aws.ToString(a.Value) - } - } - return "" -} - -func sortFindings(findings []types.ImageScanFinding) []types.ImageScanFinding { +func sortFindings(findings []finding.Detail) []finding.Detail { // shallow clone, don't affect source array sorted := slices.Clone(findings) // sort by severity rank, then CVE _descending_ - slices.SortFunc(sorted, func(a, b types.ImageScanFinding) int { + slices.SortFunc(sorted, func(a, b finding.Detail) int { sevRank := compareSeverities(a.Severity, b.Severity) if sevRank != 0 { return sevRank @@ -101,7 +89,7 @@ func sortFindings(findings []types.ImageScanFinding) []types.ImageScanFinding { // descending order of CVE, in general this means that newer CVEs will be at // the top - return strings.Compare(aws.ToString(b.Name), aws.ToString(a.Name)) + return strings.Compare(b.Name, a.Name) }) return sorted diff --git a/src/report/annotation.gohtml b/src/report/annotation.gohtml index b7045b39..85441e10 100644 --- a/src/report/annotation.gohtml +++ b/src/report/annotation.gohtml @@ -35,7 +35,7 @@ there is no indentation: indented output can be rendered differently. {{ else }}No vulnerabilities reported.
{{ end }} -{{ if .ScanFindings.Findings }} +{{ if .FindingSummary.Details }}-scan completed: {{ .ScanFindings.ImageScanCompletedAt | timeAgo }} | -source updated: {{ .ScanFindings.VulnerabilitySourceUpdatedAt | timeAgo }} +scan completed: {{ .FindingSummary.ImageScanCompletedAt | timeAgo }} | +source updated: {{ .FindingSummary.VulnerabilitySourceUpdatedAt | timeAgo }}
diff --git a/src/report/annotation_test.go b/src/report/annotation_test.go index f47545c8..c3bf7230 100644 --- a/src/report/annotation_test.go +++ b/src/report/annotation_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/cultureamp/ecrscanresults/finding" + "github.com/cultureamp/ecrscanresults/findingconfig" "github.com/cultureamp/ecrscanresults/registry" "github.com/cultureamp/ecrscanresults/report" "github.com/hexops/autogold/v2" @@ -28,7 +28,6 @@ func TestReports(t *testing.T) { Tag: "digest-value", }, ImageLabel: "", - ScanFindings: types.ImageScanFindings{}, CriticalSeverityThreshold: 0, HighSeverityThreshold: 0, }, @@ -43,7 +42,6 @@ func TestReports(t *testing.T) { Tag: "digest-value", }, ImageLabel: "label of image", - ScanFindings: types.ImageScanFindings{}, CriticalSeverityThreshold: 0, HighSeverityThreshold: 0, }, @@ -64,80 +62,36 @@ func TestReports(t *testing.T) { "AA-BOGUS-SEVERITY": {Included: 1}, "CRITICAL": {Included: 1}, }, - }, - ScanFindings: types.ImageScanFindings{ - Findings: []types.ImageScanFinding{ + Details: []finding.Detail{ { - Name: aws.String("CVE-2019-5300"), - Description: aws.String("Another vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5300"), - Severity: "AA-BOGUS-SEVERITY", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("5300-version"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("5300-package"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("10.0"), - }, - }, + Name: "CVE-2019-5300", + Description: "Another vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5300", + Severity: "AA-BOGUS-SEVERITY", + PackageName: "5300-package", + PackageVersion: "5300-version", + CVSS2Score: "10.0", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", }, { - Name: aws.String("CVE-2019-5188"), - Description: aws.String("A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5188"), - Severity: "HIGH", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("1.44.1-1ubuntu1.1"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("e2fsprogs"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("4.6"), - }, - }, + Name: "CVE-2019-5188", + Description: "A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5188", + Severity: "HIGH", + PackageName: "e2fsprogs", + PackageVersion: "1.44.1-1ubuntu1.1", + CVSS2Score: "4.6", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", }, { - Name: aws.String("CVE-2019-5200"), - Description: aws.String("Another vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5200"), - Severity: "CRITICAL", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("5200-version"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("5200-package"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("10.0"), - }, - }, + Name: "CVE-2019-5200", + Description: "Another vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5200", + Severity: "CRITICAL", + PackageName: "5200-package", + PackageVersion: "5200-version", + CVSS2Score: "10.0", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", }, }, }, @@ -161,85 +115,44 @@ func TestReports(t *testing.T) { "AA-BOGUS-SEVERITY": {Included: 0, Ignored: 1}, "CRITICAL": {Included: 1}, }, - Ignored: map[string]struct{}{ - "CVE-2019-5300": {}, - }, - }, - ScanFindings: types.ImageScanFindings{ - Findings: []types.ImageScanFinding{ + Details: []finding.Detail{ { - Name: aws.String("CVE-2019-5300"), - Description: aws.String("Another vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5300"), - Severity: "AA-BOGUS-SEVERITY", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("5300-version"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("5300-package"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("10.0"), - }, + Name: "CVE-2019-5300", + Description: "Another vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5300", + Severity: "AA-BOGUS-SEVERITY", + PackageName: "5300-package", + PackageVersion: "5300-version", + CVSS2Score: "10.0", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", + Ignore: &findingconfig.Ignore{ + ID: "CVE-2019-5300", + Until: findingconfig.MustParseUntil("2023-12-31"), + Reason: "ignored", }, }, { - Name: aws.String("CVE-2019-5188"), - Description: aws.String("A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5188"), - Severity: "HIGH", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("1.44.1-1ubuntu1.1"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("e2fsprogs"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("4.6"), - }, - }, + Name: "CVE-2019-5188", + Description: "A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5188", + Severity: "HIGH", + PackageName: "e2fsprogs", + PackageVersion: "1.44.1-1ubuntu1.1", + CVSS2Score: "4.6", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", }, { - Name: aws.String("CVE-2019-5200"), - Description: aws.String("Another vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5200"), - Severity: "CRITICAL", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("5200-version"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("5200-package"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("10.0"), - }, - }, + Name: "CVE-2019-5200", + Description: "Another vulnerability.", + Uri: "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5200", + Severity: "CRITICAL", + PackageName: "5200-package", + PackageVersion: "5200-version", + CVSS2Score: "10.0", + CVSS2Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P", }, }, + Ignored: []finding.Detail{}, }, CriticalSeverityThreshold: 0, HighSeverityThreshold: 0, diff --git a/src/report/testdata/TestReports/findings_included.golden b/src/report/testdata/TestReports/findings_included.golden index 5a1ad1c9..f7eed569 100644 --- a/src/report/testdata/TestReports/findings_included.golden +++ b/src/report/testdata/TestReports/findings_included.golden @@ -52,8 +52,6 @@No vulnerabilities reported.
{{ end }} -{{ if .FindingSummary.Details }} +{{ define "findingIgnoreUntil" }} +{{ if .Until }}{{ .Until }}{{ else }}indefinitely{{ end }} +{{ end }} +{{ define "findingIgnore"}} +{{ if .Reason }}CVE | Severity | -Effects | +Affects | CVSS score | Vector |
---|---|---|---|---|---|
{{ if $f.Uri }}{{ $f.Name }}{{ else }}{{ $f.Name }}{{ end }}{{ if $f.Ignore }} (ignored){{ end }} | +{{ if $f.Uri }}{{ $f.Name }}{{ else }}{{ $f.Name }}{{ end }}{{ if $f.Ignore }}{{ if $f.Ignore.Until }} (ignored until {{ $f.Ignore.Until }}){{ end }}{{ end }} | +{{ $f.Severity | string | lowerCase | titleCase }} | +{{ $f.PackageName | nbsp }} {{ $f.PackageVersion | nbsp }} | +{{ $f.CVSS2Score | nbsp}} | +{{ if $f.CVSS2Vector }}{{ $f.CVSS2Vector }}{{ else }} {{ end }} | +
The below findings have been ignored for the purposes of threshold calculations. See the table for details, and adjust the plugin configuration if this is incorrect.
+CVE | +Severity | +Ignored until | + | Affects | +CVSS score | +Vector | +
---|---|---|---|---|---|---|
{{ if $f.Uri }}{{ $f.Name }}{{ else }}{{ $f.Name }}{{ end }} | {{ $f.Severity | string | lowerCase | titleCase }} | +{{ template "findingIgnore" $f.Ignore }} | {{ $f.PackageName | nbsp }} {{ $f.PackageVersion | nbsp }} | {{ $f.CVSS2Score | nbsp}} | {{ if $f.CVSS2Vector }}{{ $f.CVSS2Vector }}{{ else }} {{ end }} |
CVE | Severity | -Effects | +Affects | CVSS score | Vector |
---|
No vulnerabilities reported.
+ +scan completed: | source updated: diff --git a/src/report/testdata/TestReports/no_vulnerabilities.golden b/src/report/testdata/TestReports/no_vulnerabilities.golden index 388e1d9b..4bd9caa5 100644 --- a/src/report/testdata/TestReports/no_vulnerabilities.golden +++ b/src/report/testdata/TestReports/no_vulnerabilities.golden @@ -9,6 +9,8 @@
No vulnerabilities reported.
+ +scan completed: | source updated: diff --git a/src/report/testdata/TestReports/some_findings_ignored.golden b/src/report/testdata/TestReports/some_findings_ignored.golden index 3447aa4f..2851fe1b 100644 --- a/src/report/testdata/TestReports/some_findings_ignored.golden +++ b/src/report/testdata/TestReports/some_findings_ignored.golden @@ -15,7 +15,7 @@
CVE | Severity | -Effects | +Affects | CVSS score | Vector | AV:L/AC:L/Au:N/C:P/I:P/A:P | +
---|
The below findings have been ignored for the purposes of threshold calculations. See the table for details, and adjust the plugin configuration if this is incorrect.
+CVE-2019-5300 (ignored) | -Aa-Bogus-Severity | +CVE | +Severity | +Ignored until | + | Affects | +CVSS score | +Vector | +
---|---|---|---|---|---|---|---|---|
CVE-2019-5300 | +Critical | +
++2023-12-31 +Ignored to give the base image a chance to be updated |
5300-package 5300-version | 10.0 | AV:L/AC:L/Au:N/C:P/I:P/A:P | |||
CVE-2023-100 | +Low | ++ +indefinitely + + | +100-package 100-version | +4.0 | +AV:L/AC:L/Au:N/C:P/I:P/A:P | +
No vulnerabilities reported.
{{ end }} +{{ define "findingNameLink"}} +{{ if .Uri }}{{ .Name }}{{ else }}{{ .Name }}{{ end }} +{{ end }} +{{ define "findingName"}} +{{ if .Description }}The below findings have been ignored for the purposes of threshold calculations. See the table for details, and adjust the plugin configuration if this is incorrect.
+The below findings have been ignored for the purposes of threshold calculations. See the table for details, and adjust the plugin configuration if this is incorrect.
CVE | Severity | -Ignored until | + | Ignored until | Affects | CVSS score | Vector | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{ if $f.Uri }}{{ $f.Name }}{{ else }}{{ $f.Name }}{{ end }} | +{{ template "findingName" . }} | {{ $f.Severity | string | lowerCase | titleCase }} | {{ template "findingIgnore" $f.Ignore }} | {{ $f.PackageName | nbsp }} {{ $f.PackageVersion | nbsp }} | diff --git a/src/report/testdata/TestReports/findings_included.golden b/src/report/testdata/TestReports/findings_included.golden index 48e2f938..8d799a60 100644 --- a/src/report/testdata/TestReports/findings_included.golden +++ b/src/report/testdata/TestReports/findings_included.golden @@ -42,6 +42,8 @@ + +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CVE-2019-5200 | +
++CVE-2019-5200 +Another vulnerability. |
Critical | 5200-package 5200-version | 10.0 | @@ -64,7 +70,11 @@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CVE-2019-5188 | +
++CVE-2019-5188 +A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
High | e2fsprogs 1.44.1-1ubuntu1.1 | 4.6 | @@ -72,7 +82,11 @@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CVE-2019-5300 | +
++CVE-2019-5300 +Another vulnerability. |
Aa-Bogus-Severity | 5300-package 5300-version | 10.0 | diff --git a/src/report/testdata/TestReports/image_label.golden b/src/report/testdata/TestReports/image_label.golden index 187ce2d9..42a08899 100644 --- a/src/report/testdata/TestReports/image_label.golden +++ b/src/report/testdata/TestReports/image_label.golden @@ -12,6 +12,8 @@ + +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CVE-2019-5200 | +
++CVE-2019-5200 +Another vulnerability. |
Critical | 5200-package 5200-version | 10.0 | @@ -64,7 +70,11 @@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CVE-2019-5188 | +
++CVE-2019-5188 +A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
High | e2fsprogs 1.44.1-1ubuntu1.1 | 4.6 | @@ -76,19 +86,23 @@
CVE | Severity | -Ignored until | + | Ignored until | Affects | CVSS score | Vector |
---|---|---|---|---|---|---|---|
CVE-2019-5300 | +
++CVE-2019-5300 +Another vulnerability. |
Critical |
@@ -101,11 +115,15 @@ | ||||
CVE-2023-100 | +
++CVE-2023-100 +A vulnerability present in some software but isn't that bad. |
Low |
-indefinitely
+ (indefinitely)
|
100-package 100-version | From 2d076a4cb41580d83eeea26adba0dd11f91e6e5f Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:52:35 +1100 Subject: [PATCH 10/25] fix: simplify ignore parsing by only accepting map This potentially leads to more discoverability that it is possible to add `until` and `reason` fields. --- src/findingconfig/ignores.go | 40 +------------------------------ src/findingconfig/ignores_test.go | 15 ++++++++---- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/findingconfig/ignores.go b/src/findingconfig/ignores.go index be2ec481..8d5eccfe 100644 --- a/src/findingconfig/ignores.go +++ b/src/findingconfig/ignores.go @@ -10,51 +10,13 @@ import ( "gopkg.in/yaml.v3" ) -// detailedIgnore represents the detailed encoding of Ignore, used -// for deserialization. Fields must match Ignore. -type detailedIgnore struct { - ID string `yaml:"id"` - Until UntilTime - Reason string -} - // An entry in an ignore file used by the plugin. type Ignore struct { - ID string + ID string `yaml:"id"` Until UntilTime Reason string } -// UnmarshalYAML is a custom YAML unmarshaller that supports a simple string -// encoding and full encoding that specifies all fields. The simple encoding is -// the string ID, the complex version allows the full ID, Until and Reason -// triple. -func (f *Ignore) UnmarshalYAML(value *yaml.Node) error { - var deserialized Ignore - switch value.Kind { - case yaml.ScalarNode: - // simple string value, interpret as ID - deserialized.ID = value.Value - - case yaml.MappingNode: - // interpret mapping as the full version with all fields - var fields detailedIgnore - err := value.Decode(&fields) - if err != nil { - return err - } - - deserialized = Ignore(fields) - - default: - return fmt.Errorf("unknown type for ignore entry (%d) at line %d:%d", value.Kind, value.Line, value.Column) - } - - *f = deserialized - - return nil -} - type Ignores struct { Ignores []Ignore } diff --git a/src/findingconfig/ignores_test.go b/src/findingconfig/ignores_test.go index f01caeb8..60840433 100644 --- a/src/findingconfig/ignores_test.go +++ b/src/findingconfig/ignores_test.go @@ -16,7 +16,7 @@ ignores: - id: CVE-2023-1234 until: 2015-02-15 reason: We don't talk about CVE-2023-1234 - - CVE-2023-9876 + - id: CVE-2023-9876 ` f := createIgnoreFile(t, in) @@ -40,7 +40,7 @@ func TestLoadIgnores_Fails(t *testing.T) { ignores: - ["nested array"] `, - expectedError: "unknown type for ignore entry", + expectedError: "cannot unmarshal !!seq", }, { in: ` @@ -56,6 +56,13 @@ ignores: `, expectedError: "did not match the expected YYYY-MM-dd format", }, + { + in: ` +ignores: + - idd: CVE-123 +`, + expectedError: "field idd not found in type", + }, } for i, c := range cases { @@ -78,7 +85,7 @@ ignores: ignores: ~`, ` ignores: - - first-issue + - id: first-issue - id: second-issue reason: second issue earliest definition `, @@ -86,7 +93,7 @@ ignores: ignores: - id: second-issue reason: second issue this reason should override earlier ones -- third-issue +- id: third-issue `, } From 8e838326eda0f0b167c914dd9b2ad01f4f8b1d7a Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:55:09 +1100 Subject: [PATCH 11/25] docs: typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10dbdde1..4b86e65a 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ for more information. Refer to how to set your [max-criticals](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-criticals-optional-string), and [max-highs](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-highs-optional-string). -### Are there guidelines on using up? +### Are there guidelines on using thresholds? Yes. Changing the `max-criticals` and `max-high` settings should not be taken lightly. From c9d25e92ea264f3145e9a1c8b55745d411d3fdd3 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:12:19 +1100 Subject: [PATCH 12/25] fix: filtered expired finding config entries Remove on read so expired items don't affect cascades. --- src/findingconfig/clock.go | 15 +++++++++++++++ src/findingconfig/ignores.go | 17 +++++++++++++---- src/findingconfig/ignores_test.go | 27 ++++++++++++++++++++++----- src/main.go | 2 +- 4 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 src/findingconfig/clock.go diff --git a/src/findingconfig/clock.go b/src/findingconfig/clock.go new file mode 100644 index 00000000..d016122c --- /dev/null +++ b/src/findingconfig/clock.go @@ -0,0 +1,15 @@ +package findingconfig + +import "time" + +type SystemClock func() time.Time + +func (c SystemClock) UtcNow() time.Time { + return c() +} + +func DefaultSystemClock() SystemClock { + return SystemClock(func() time.Time { + return time.Now().UTC() + }) +} diff --git a/src/findingconfig/ignores.go b/src/findingconfig/ignores.go index 8d5eccfe..757918e0 100644 --- a/src/findingconfig/ignores.go +++ b/src/findingconfig/ignores.go @@ -6,6 +6,7 @@ import ( "os" "slices" "strings" + "time" "gopkg.in/yaml.v3" ) @@ -24,7 +25,7 @@ type Ignores struct { // LoadExistingIgnores uses LoadIgnores to read any of the given set of files // that exist. Repeated definitions for the same "Name" will overwrite each // other. The later definition will take precedence. -func LoadExistingIgnores(filenames []string) ([]Ignore, error) { +func LoadExistingIgnores(filenames []string, clock SystemClock) ([]Ignore, error) { ignores := []Ignore{} for _, name := range filenames { @@ -40,7 +41,7 @@ func LoadExistingIgnores(filenames []string) ([]Ignore, error) { continue } - partial, err := LoadIgnores(name) + partial, err := LoadIgnores(name, clock) if err != nil { return nil, fmt.Errorf("loading ignore file '%s' failed: %w", name, err) } @@ -64,7 +65,7 @@ func LoadExistingIgnores(filenames []string) ([]Ignore, error) { } // LoadIgnores parses a YAML ignore file from the given location. -func LoadIgnores(filename string) ([]Ignore, error) { +func LoadIgnores(filename string, clock SystemClock) ([]Ignore, error) { content, err := os.ReadFile(filename) if err != nil { return nil, err @@ -76,7 +77,15 @@ func LoadIgnores(filename string) ([]Ignore, error) { return nil, err } - return i.Ignores, nil + filtered := slices.DeleteFunc(i.Ignores, func(ignore Ignore) bool { + u := time.Time(ignore.Until) + z := u.IsZero() + d := !z && clock.UtcNow().After(u) + + return d + }) + + return filtered, nil } // unmarshalYAML decodes a YAML encoded byte stream into the supplied pointer diff --git a/src/findingconfig/ignores_test.go b/src/findingconfig/ignores_test.go index 60840433..4a3e76f6 100644 --- a/src/findingconfig/ignores_test.go +++ b/src/findingconfig/ignores_test.go @@ -4,27 +4,31 @@ import ( "fmt" "os" "testing" + "time" "github.com/cultureamp/ecrscanresults/findingconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var defaultTime = "2023-12-01" +var defaultTestClock = testClock(defaultTime) + func TestLoadIgnores_Succeeds(t *testing.T) { in := ` ignores: - id: CVE-2023-1234 - until: 2015-02-15 + until: 2024-02-15 reason: We don't talk about CVE-2023-1234 - id: CVE-2023-9876 ` f := createIgnoreFile(t, in) - i, err := findingconfig.LoadIgnores(f) + i, err := findingconfig.LoadIgnores(f, defaultTestClock) require.NoError(t, err) assert.Equal(t, []findingconfig.Ignore{ - {ID: "CVE-2023-1234", Until: findingconfig.MustParseUntil("2015-02-15"), Reason: "We don't talk about CVE-2023-1234"}, + {ID: "CVE-2023-1234", Until: findingconfig.MustParseUntil("2024-02-15"), Reason: "We don't talk about CVE-2023-1234"}, {ID: "CVE-2023-9876", Until: findingconfig.UntilTime{}, Reason: ""}, }, i) } @@ -69,7 +73,7 @@ ignores: t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { f := createIgnoreFile(t, c.in) - _, err := findingconfig.LoadIgnores(f) + _, err := findingconfig.LoadIgnores(f, defaultTestClock) require.ErrorContains(t, err, c.expectedError) }) } @@ -88,22 +92,29 @@ ignores: - id: first-issue - id: second-issue reason: second issue earliest definition + - id: fourth-issue + reason: still valid, should take precedence + until: 2023-12-31 `, ` ignores: - id: second-issue reason: second issue this reason should override earlier ones - id: third-issue +- id: fourth-issue + reason: expired should not override other 'forth-issue' + until: 2023-11-30 `, } files := createIgnoreFiles(t, contents) - actual, err := findingconfig.LoadExistingIgnores(files) + actual, err := findingconfig.LoadExistingIgnores(files, defaultTestClock) require.NoError(t, err) assert.Equal(t, []findingconfig.Ignore{ {ID: "first-issue", Until: findingconfig.UntilTime{}, Reason: ""}, + {ID: "fourth-issue", Until: findingconfig.MustParseUntil("2023-12-31"), Reason: "still valid, should take precedence"}, {ID: "second-issue", Until: findingconfig.UntilTime{}, Reason: "second issue this reason should override earlier ones"}, {ID: "third-issue", Until: findingconfig.UntilTime{}, Reason: ""}, }, actual) @@ -137,3 +148,9 @@ func createIgnoreFile(t *testing.T, contents string) string { return f.Name() } + +func testClock(yyyMMdd string) findingconfig.SystemClock { + now := time.Time(findingconfig.MustParseUntil(yyyMMdd)) + + return findingconfig.SystemClock(func() time.Time { return now }) +} diff --git a/src/main.go b/src/main.go index 0d8dcbd8..a55826ca 100644 --- a/src/main.go +++ b/src/main.go @@ -119,7 +119,7 @@ func runCommand(ctx context.Context, pluginConfig Config, agent buildkite.Agent) "buildkite/ecr-scan-results-ignore.yml", "~/.ecr-scan-results-ignore.yaml", "~/.ecr-scan-results-ignore.yml", - }) + }, findingconfig.DefaultSystemClock()) if err != nil { return runtimeerrors.NonFatal("could not load finding ignore configuration", err) } From 3f47c324767edc93a6ba34850bde49288035641c Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:57:58 +1100 Subject: [PATCH 13/25] fix: correct casing of URI to align with linter --- src/finding/summary.go | 4 ++-- src/report/annotation.gohtml | 2 +- src/report/annotation_test.go | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/finding/summary.go b/src/finding/summary.go index 9091beb0..e7bc69bd 100644 --- a/src/finding/summary.go +++ b/src/finding/summary.go @@ -13,7 +13,7 @@ type Detail struct { // The name associated with the finding, usually a CVE number. Name string - Uri string + URI string // The description of the finding. Description string @@ -119,7 +119,7 @@ func Summarize(findings *types.ImageScanFindings, ignoreConfig []findingconfig.I func findingToDetail(finding types.ImageScanFinding) Detail { return Detail{ Name: aws.ToString(finding.Name), - Uri: aws.ToString(finding.Uri), + URI: aws.ToString(finding.Uri), Description: aws.ToString(finding.Description), Severity: finding.Severity, PackageName: findingAttributeValue(finding, "package_name"), diff --git a/src/report/annotation.gohtml b/src/report/annotation.gohtml index 7e8acd82..1667cc4c 100644 --- a/src/report/annotation.gohtml +++ b/src/report/annotation.gohtml @@ -36,7 +36,7 @@ there is no indentation: indented output can be rendered differently.|||
--CVE-2019-5200 -Another vulnerability. |
+CVE-2019-5200Another vulnerability. |
Critical | 5200-package 5200-version | 10.0 | @@ -70,11 +66,7 @@|||
--CVE-2019-5188 -A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
+CVE-2019-5188A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
High | e2fsprogs 1.44.1-1ubuntu1.1 | 4.6 | @@ -82,11 +74,7 @@|||
--CVE-2019-5300 -Another vulnerability. |
+CVE-2019-5300Another vulnerability. |
Aa-Bogus-Severity | 5300-package 5300-version | 10.0 | diff --git a/src/report/testdata/TestReports/some_findings_ignored.golden b/src/report/testdata/TestReports/some_findings_ignored.golden index 0b0ba825..80e5bb98 100644 --- a/src/report/testdata/TestReports/some_findings_ignored.golden +++ b/src/report/testdata/TestReports/some_findings_ignored.golden @@ -58,11 +58,7 @@|||
--CVE-2019-5200 -Another vulnerability. |
+CVE-2019-5200Another vulnerability. |
Critical | 5200-package 5200-version | 10.0 | @@ -70,11 +66,7 @@|||
--CVE-2019-5188 -A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
+CVE-2019-5188A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. |
High | e2fsprogs 1.44.1-1ubuntu1.1 | 4.6 | @@ -98,34 +90,18 @@|||
--CVE-2019-5300 -Another vulnerability. |
+CVE-2019-5300Another vulnerability. |
Critical | -
--2023-12-31 -Ignored to give the base image a chance to be updated |
+2023-12-31Ignored to give the base image a chance to be updated |
5300-package 5300-version | 10.0 | AV:L/AC:L/Au:N/C:P/I:P/A:P |
--CVE-2023-100 -A vulnerability present in some software but isn't that bad. |
+CVE-2023-100A vulnerability present in some software but isn't that bad. |
Low | -
-
- (indefinitely)
-
- |
+(indefinitely) |
100-package 100-version | 4.0 | AV:L/AC:L/Au:N/C:P/I:P/A:P | From b934c560aa8683b4887c027bc1cb613037630ca6 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:48:51 +1100 Subject: [PATCH 15/25] docs: add docs for ignore file specification --- README.md | 10 +++++-- docs/ignore-findings.md | 44 +++++++++++++++++++++++++++++++ docs/img/ignore-finding-list.png | Bin 0 -> 188850 bytes docs/img/ignore-reason.png | Bin 0 -> 37684 bytes docs/img/summary-counts.png | Bin 0 -> 33907 bytes 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 docs/ignore-findings.md create mode 100644 docs/img/ignore-finding-list.png create mode 100644 docs/img/ignore-reason.png create mode 100644 docs/img/summary-counts.png diff --git a/README.md b/README.md index 4b86e65a..baa94b3d 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ service. By default the plugin will cause the step to fail if there are critical or high vulnerabilities reported, but there are configurable thresholds on this behaviour. -> ℹ️ TIP: if you want the build to continue when vulnerabilities are found, be +> ℹ️ **TIP**: if you want the build to continue when vulnerabilities are found, be > sure to supply values for `max-criticals` and `max-highs` parameters. If these > are set to high values your build will never fail, but details will be > supplied in the annotation. > -> Check out the FAQs below for more information +> If a finding is irrelevant, or you're waiting on an upstream fix, use an "ignore" configuration file instead: see the [ignore findings](./docs/ignore-findings.md) documentation. ## Example @@ -76,12 +76,18 @@ If the number of critical vulnerabilities in the image exceeds this threshold the build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999) to allow the build to always pass. +> [!IMPORTANT] +> Prefer an [ignore file](./docs/ignore-findings.md) over setting thresholds if a finding is irrelevant or time to respond is required. + ### `max-highs` (Optional, string) If the number of high vulnerabilities in the image exceeds this threshold the build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999) to allow the build to always pass. +> [!IMPORTANT] +> Prefer an [ignore file](./docs/ignore-findings.md) over setting thresholds if a finding is irrelevant or time to respond is required. + ### `image-label` (Optional, string) When supplied, this is used to title the report annotation in place of the diff --git a/docs/ignore-findings.md b/docs/ignore-findings.md new file mode 100644 index 00000000..3f4b5c16 --- /dev/null +++ b/docs/ignore-findings.md @@ -0,0 +1,44 @@ +# Ignoring findings + +Findings can be ignored using a YAML file with the following structure: + +```yaml +ignores: + - id: CVE-2023-100 + - id: CVE-2023-200 + until: 2023-12-31 + reason: allowing 2 weeks for base image to update + - id: CVE-2023-300 +``` + +- each element must have at least the `id` field +- the `until` field defines the expiry of this ignore entry. This allows a team time to respond while temporarily allowing builds to continue. +- the `reason` field gives a justification that is rendered in the annotation for greater visibility. Including the "why" in this field is highly recommended. + +Ignore configuration can be specified in a number of places. If a listing for a finding with the same CVE name appears in multiple files, the most local wins: central configuration can be overridden by the repository. + +From least to most important: + +- `/etc/ecr-scan-results-buildkite-plugin/ignore.y[a]ml` (part of the agent, not modifiable by builds) +- `buildkite/ecr-scan-results-ignore.y[a]ml` (specified alongside the pipeline) +- `.buildkite/ecr-scan-results-ignore.y[a]ml` +- `.ecr-scan-results-ignore.y[a]ml` (local repository configuration) + +Configuration in the `/etc/ecr-scan-results-buildkite-plugin` directory allows for organizations to ship agents with plugin configuration that centrally manages findings that can be ignored. + +> [!IMPORTANT] +> When a finding is ignored, it is removed from consideration for threshold checks, but it's not discarded. The annotation created by the plugin adds details to the results, giving high visibility on the configured behaviour. + +## Rendering + +The summary counts at the top show the number of ignored findings: + + + +Ignored findings are separated from the main list and shown at the bottom: + + + +If a reason for ignoring a finding is provided, it's made available by expanding the Until date: + + diff --git a/docs/img/ignore-finding-list.png b/docs/img/ignore-finding-list.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cc9d95bbb1d6702e7ea8923ec8f52343b9473e GIT binary patch literal 188850 zcmeEucR1Vs-gm21OG{Nz6kTSDU+t|+)utq|LQBmEYHy`QY0=i+dxoU;45db`#EOvE zo7l7V^F8---_LW-xq9w%|M~p!xUS^-Cg0&R-{bXqy%VafseF}&jpp3Bb5~VfJbQWW z9F5Gma}=GID1dM1yu-WBojY%0^Yp2<%G0N}wVfO+ZR{-0o#Xq8_)?-q@%m<48tCWZ zeUbZYZ1yx1jM7QO?jMxYCE3^6uv)V3w-2l9E>)%1aA)RNZqk3Wv%bcbn0=ei+Llaz z$)3MvTnkijI5~H)xzVt`zjf~E`Eci-)ahn25Q&;>JG*(NGte9LyD!|HnM*`kiP)I= zkSCqF?p*ebXQ@LKR&4hzkt{*3!?j4$)RR>%5A)V