diff --git a/cmd/lava/internal/help/helpdoc.go b/cmd/lava/internal/help/helpdoc.go index fcfc2af..7f8fcd5 100644 --- a/cmd/lava/internal/help/helpdoc.go +++ b/cmd/lava/internal/help/helpdoc.go @@ -147,6 +147,9 @@ the following properties. - exclusions: list of rules that define what findings should be excluded from the report. It allows to ignore findings because of accepted risks, false positives, etc. + - errorOnStaleExclusions: boolean specifying whether Lava should + exit with error when stale exclusions are detected. If not + specified, the default value is false. The sample below is a full report configuration: @@ -160,6 +163,7 @@ The sample below is a full report configuration: - description: Ignore test certificates. summary: 'Secret Leaked in Git Repository' resource: '/testdata/certs/' + errorOnStaleExclusions: true The exclusion rules support the following filters: diff --git a/cmd/lava/internal/scan/scan.go b/cmd/lava/internal/scan/scan.go index fc4dc81..e0fa240 100644 --- a/cmd/lava/internal/scan/scan.go +++ b/cmd/lava/internal/scan/scan.go @@ -36,6 +36,7 @@ that have been found. - 1: Command error - 2: Syntax error - 3: Check error + - 4: Stale exclusions - 100: Informational vulnerabilities found - 101: Low severity vulnerabilities found - 102: Medium severity vulnerabilities found diff --git a/internal/config/config.go b/internal/config/config.go index 30b5aa8..774d23c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -189,6 +189,10 @@ type ReportConfig struct { // instance, accepted risks, false positives, etc. Exclusions []Exclusion `yaml:"exclusions"` + // ErrorOnStaleExclusions specifies whether Lava should exit + // with error when stale exclusions are detected. + ErrorOnStaleExclusions bool `yaml:"errorOnStaleExclusions"` + // Metrics is the file where the metrics will be written. // If Metrics is an empty string or not specified in the yaml file, then // the metrics report is not saved. diff --git a/internal/report/human.tmpl b/internal/report/human.tmpl index 0e8a106..88f0be7 100644 --- a/internal/report/human.tmpl +++ b/internal/report/human.tmpl @@ -5,6 +5,9 @@ {{- if .Vulns}} {{template "vulns" . -}} {{end -}} +{{- if .StaleExcls}} +{{template "staleExcls" . -}} +{{end -}} {{- end -}} @@ -158,10 +161,41 @@ Number of excluded vulnerabilities not included in the summary table: {{.Exclude {{- $rsc := . -}} - {{$rsc.Name | bold}}: {{- range $row := $rsc.Rows}}{{range $i, $header := $rsc.Header}} - {{if eq $i 0}}- {{else}} {{end}}{{ $header | trim | bold}}: {{index $row $header | trim -}} + {{if eq $i 0}}- {{else}} {{end}}{{$header | trim | bold}}: {{index $row $header | trim -}} {{end}}{{end}} {{- end -}} +{{- /* staleExcls is the template used to render the details of the stale exclusions. */ -}} +{{- define "staleExcls" -}} +{{"STALE EXCLUSIONS" | bold | underline}} + +{{range $excl := .StaleExcls}} +{{- template "excl" . -}} +{{end}} +{{- end -}} + +{{- /* excl is the template used to render an exclusion. */ -}} +{{- define "excl" -}} +{{- $pref := "- " -}} +{{- if .Target}} +{{- $pref}}{{"Target" | bold}}: {{.Target | trim}}{{$pref = " "}} +{{end -}} +{{- if .Description}} +{{- $pref}}{{"Description" | bold}}: {{.Description | trim}}{{$pref = " "}} +{{end -}} +{{- if .Resource}} +{{- $pref}}{{"Resource" | bold}}: {{.Resource | trim}}{{$pref = " "}} +{{end -}} +{{- if .Fingerprint}} +{{- $pref}}{{"Fingerprint" | bold}}: {{.Fingerprint | trim}}{{$pref = " "}} +{{end -}} +{{- if .Summary}} +{{- $pref}}{{"Summary" | bold}}: {{.Summary | trim}}{{$pref = " "}} +{{end -}} +{{- if not .ExpirationDate.IsZero}} +{{- $pref}}{{"Expiration Date" | bold}}: {{.ExpirationDate.String | trim}}{{$pref = " "}} +{{end -}} +{{- end -}} {{- /* Render the report. */ -}} {{- template "report" . -}} diff --git a/internal/report/humanprinter.go b/internal/report/humanprinter.go index 07ec66a..80b2264 100644 --- a/internal/report/humanprinter.go +++ b/internal/report/humanprinter.go @@ -40,7 +40,7 @@ var ( ) // Print renders the scan results in a human-readable format. -func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, summ summary, status []checkStatus) error { +func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, summ summary, status []checkStatus, staleExcls []config.Exclusion) error { // count the total non-excluded vulnerabilities found. var total int for _, ss := range summ.count { @@ -53,17 +53,20 @@ func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, summ summary, } data := struct { - Stats map[string]int - Total int - Excluded int - Vulns []vulnerability - Status []checkStatus + Stats map[string]int + Total int + Excluded int + Vulns []vulnerability + Status []checkStatus + AllExclMatched bool + StaleExcls []config.Exclusion }{ - Stats: stats, - Total: total, - Excluded: summ.excluded, - Vulns: vulns, - Status: status, + Stats: stats, + Total: total, + Excluded: summ.excluded, + Vulns: vulns, + Status: status, + StaleExcls: staleExcls, } if err := humanTmpl.Execute(w, data); err != nil { diff --git a/internal/report/humanprinter_test.go b/internal/report/humanprinter_test.go index 42fd092..988c0c0 100644 --- a/internal/report/humanprinter_test.go +++ b/internal/report/humanprinter_test.go @@ -18,6 +18,7 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { vulnerabilities []vulnerability summ summary status []checkStatus + staleExcls []config.Exclusion want []string }{ { @@ -162,6 +163,9 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { Status: "FINISHED", }, }, + staleExcls: []config.Exclusion{ + {Summary: "Unused exclusion"}, + }, want: []string{ "STATUS", "FINISHED", @@ -169,6 +173,8 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { "Number of excluded vulnerabilities not included in the summary table: 3", "VULNERABILITIES", "Vulnerability Summary 1", + "STALE EXCLUSIONS", + "- Summary: Unused exclusion", }, }, { @@ -193,7 +199,7 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer w := humanPrinter{} - if err := w.Print(&buf, tt.vulnerabilities, tt.summ, tt.status); err != nil { + if err := w.Print(&buf, tt.vulnerabilities, tt.summ, tt.status, tt.staleExcls); err != nil { t.Errorf("unexpected error value: %v", err) } text := buf.String() diff --git a/internal/report/jsonprinter.go b/internal/report/jsonprinter.go index 42061a9..ecc81c0 100644 --- a/internal/report/jsonprinter.go +++ b/internal/report/jsonprinter.go @@ -6,13 +6,15 @@ import ( "encoding/json" "fmt" "io" + + "github.com/adevinta/lava/internal/config" ) // jsonPrinter represents a JSON report printer. type jsonPrinter struct{} // Print renders the scan results in JSON format. -func (prn jsonPrinter) Print(w io.Writer, vulns []vulnerability, _ summary, _ []checkStatus) error { +func (prn jsonPrinter) Print(w io.Writer, vulns []vulnerability, _ summary, _ []checkStatus, _ []config.Exclusion) error { enc := json.NewEncoder(w) enc.SetIndent("", " ") if err := enc.Encode(vulns); err != nil { diff --git a/internal/report/jsonprinter_test.go b/internal/report/jsonprinter_test.go index cc034be..11d502e 100644 --- a/internal/report/jsonprinter_test.go +++ b/internal/report/jsonprinter_test.go @@ -163,7 +163,7 @@ func TestJsonPrinter_Print(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer w := jsonPrinter{} - err := w.Print(&buf, tt.vulnerabilities, summary{}, nil) + err := w.Print(&buf, tt.vulnerabilities, summary{}, nil, nil) if (err == nil) != tt.wantNilErr { t.Errorf("unexpected error value: %v", err) } diff --git a/internal/report/report.go b/internal/report/report.go index 2c532e9..3fc0f3f 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -23,12 +23,13 @@ import ( // Writer represents a Lava report writer. type Writer struct { - prn printer - w io.WriteCloser - isStdout bool - minSeverity config.Severity - showSeverity config.Severity - exclusions []config.Exclusion + prn printer + w io.WriteCloser + isStdout bool + minSeverity config.Severity + showSeverity config.Severity + exclusions []config.Exclusion + errorOnStaleExclusions bool } // timeNow is set by tests to mock the current time. @@ -65,12 +66,13 @@ func NewWriter(cfg config.ReportConfig) (Writer, error) { } return Writer{ - prn: prn, - w: w, - isStdout: isStdout, - minSeverity: cfg.Severity, - showSeverity: showSeverity, - exclusions: cfg.Exclusions, + prn: prn, + w: w, + isStdout: isStdout, + minSeverity: cfg.Severity, + showSeverity: showSeverity, + exclusions: cfg.Exclusions, + errorOnStaleExclusions: cfg.ErrorOnStaleExclusions, }, nil } @@ -92,17 +94,37 @@ func (writer Writer) Write(er engine.Report) (ExitCode, error) { metrics.Collect("excluded_vulnerability_count", summ.excluded) metrics.Collect("vulnerability_count", summ.count) + staleExcls := writer.getStaleExclusions(vulns) + fvulns := writer.filterVulns(vulns) status := mkStatus(er) - exitCode := writer.calculateExitCode(summ, status) + exitCode := writer.calculateExitCode(summ, status, staleExcls) - if err = writer.prn.Print(writer.w, fvulns, summ, status); err != nil { + if err = writer.prn.Print(writer.w, fvulns, summ, status, staleExcls); err != nil { return exitCode, fmt.Errorf("print report: %w", err) } return exitCode, nil } +// getStaleExclusions returns the list of stale exclusions. +func (writer Writer) getStaleExclusions(vulns []vulnerability) []config.Exclusion { + m := make(map[int]struct{}) + for _, vuln := range vulns { + for _, idx := range vuln.matchedExclusions { + m[idx] = struct{}{} + } + } + + var staleExcls []config.Exclusion + for i, excl := range writer.exclusions { + if _, ok := m[i]; !ok { + staleExcls = append(staleExcls, excl) + } + } + return staleExcls +} + // Close closes the [Writer]. func (writer Writer) Close() error { if !writer.isStdout { @@ -122,15 +144,15 @@ func (writer Writer) parseReport(er engine.Report) ([]vulnerability, error) { for _, r := range er { for _, vuln := range r.ResultData.Vulnerabilities { severity := scoreToSeverity(vuln.Score) - excluded, err := writer.isExcluded(vuln, r.Target) + excls, err := writer.matchExclusions(vuln, r.Target) if err != nil { return nil, fmt.Errorf("vulnerability exlusion: %w", err) } v := vulnerability{ - CheckData: r.CheckData, - Vulnerability: vuln, - Severity: severity, - excluded: excluded, + CheckData: r.CheckData, + Vulnerability: vuln, + Severity: severity, + matchedExclusions: excls, } vulns = append(vulns, v) } @@ -138,10 +160,15 @@ func (writer Writer) parseReport(er engine.Report) ([]vulnerability, error) { return vulns, nil } -// isExcluded returns whether the provided [report.Vulnerability] is -// excluded based on the [Writer] configuration and the affected target. -func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, error) { - for _, excl := range writer.exclusions { +// matchExclusions is responsible for determining if a given [report.Vulnerability] +// should be excluded based on predefined exclusion criteria. The method +// compares the [report.Vulnerability] against a list of exclusions stored +// in the [Writer] and returns a slice of integers representing the indices of +// the exclusions that match the vulnerability. If any errors occur during the +// matching process, an error is returned. +func (writer Writer) matchExclusions(v report.Vulnerability, target string) (excls []int, err error) { + var exclusions []int + for i, excl := range writer.exclusions { if !excl.ExpirationDate.IsZero() && excl.ExpirationDate.Before(timeNow()) { continue } @@ -153,7 +180,7 @@ func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, er if excl.Summary != "" { matched, err := regexp.MatchString(excl.Summary, v.Summary) if err != nil { - return false, fmt.Errorf("match string: %w", err) + return nil, fmt.Errorf("match string: %w", err) } if !matched { continue @@ -163,7 +190,7 @@ func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, er if excl.Target != "" { matched, err := regexp.MatchString(excl.Target, target) if err != nil { - return false, fmt.Errorf("match string: %w", err) + return nil, fmt.Errorf("match string: %w", err) } if !matched { continue @@ -173,19 +200,19 @@ func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, er if excl.Resource != "" { matchedResource, err := regexp.MatchString(excl.Resource, v.AffectedResource) if err != nil { - return false, fmt.Errorf("match string: %w", err) + return nil, fmt.Errorf("match string: %w", err) } matchedResourceString, err := regexp.MatchString(excl.Resource, v.AffectedResourceString) if err != nil { - return false, fmt.Errorf("match string: %w", err) + return nil, fmt.Errorf("match string: %w", err) } if !matchedResource && !matchedResourceString { continue } } - return true, nil + exclusions = append(exclusions, i) } - return false, nil + return exclusions, nil } // filterVulns takes a list of vulnerabilities and filters out those @@ -204,7 +231,7 @@ func (writer Writer) filterVulns(vulns []vulnerability) []vulnerability { if v.Severity < writer.showSeverity { break } - if v.excluded { + if v.isExcluded() { continue } fvulns = append(fvulns, v) @@ -217,13 +244,17 @@ func (writer Writer) filterVulns(vulns []vulnerability) []vulnerability { // min severity configured in the writer. For that it makes use of the summary. // // See [ExitCode] for more information about exit codes. -func (writer Writer) calculateExitCode(summ summary, status []checkStatus) ExitCode { +func (writer Writer) calculateExitCode(summ summary, status []checkStatus, staleExcl []config.Exclusion) ExitCode { for _, cs := range status { if cs.Status != "FINISHED" { return ExitCodeCheckError } } + if writer.errorOnStaleExclusions && len(staleExcl) > 0 { + return ExitCodeStaleExclusions + } + for sev := config.SeverityCritical; sev >= writer.minSeverity; sev-- { if summ.count[sev] > 0 { diff := sev - config.SeverityInfo @@ -236,14 +267,20 @@ func (writer Writer) calculateExitCode(summ summary, status []checkStatus) ExitC // vulnerability represents a vulnerability found by a check. type vulnerability struct { report.Vulnerability - CheckData report.CheckData `json:"check_data"` - Severity config.Severity `json:"severity"` - excluded bool + CheckData report.CheckData `json:"check_data"` + Severity config.Severity `json:"severity"` + matchedExclusions []int +} + +// isExclude reports whether the [vulnerability] should be excluded +// from the report. +func (vuln vulnerability) isExcluded() bool { + return len(vuln.matchedExclusions) > 0 } // A printer renders a Vulcan report in a specific format. type printer interface { - Print(w io.Writer, vulns []vulnerability, summ summary, status []checkStatus) error + Print(w io.Writer, vulns []vulnerability, summ summary, status []checkStatus, staleExcls []config.Exclusion) error } // scoreToSeverity converts a CVSS score into a [config.Severity]. @@ -287,7 +324,7 @@ func mkSummary(vulns []vulnerability) (summary, error) { if !vuln.Severity.IsValid() { return summary{}, fmt.Errorf("invalid severity: %v", vuln.Severity) } - if vuln.excluded { + if vuln.isExcluded() { summ.excluded++ } else { summ.count[vuln.Severity]++ @@ -324,10 +361,11 @@ type ExitCode int // Exit codes depending on the maximum severity found. const ( - ExitCodeCheckError ExitCode = 3 - ExitCodeInfo ExitCode = 100 - ExitCodeLow ExitCode = 101 - ExitCodeMedium ExitCode = 102 - ExitCodeHigh ExitCode = 103 - ExitCodeCritical ExitCode = 104 + ExitCodeCheckError ExitCode = 3 + ExitCodeStaleExclusions ExitCode = 4 + ExitCodeInfo ExitCode = 100 + ExitCodeLow ExitCode = 101 + ExitCodeMedium ExitCode = 102 + ExitCodeHigh ExitCode = 103 + ExitCodeCritical ExitCode = 104 ) diff --git a/internal/report/report_test.go b/internal/report/report_test.go index b8189a2..32fcd03 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path" + "slices" "testing" "time" @@ -19,11 +20,12 @@ import ( func TestWriter_calculateExitCode(t *testing.T) { tests := []struct { - name string - summ summary - status []checkStatus - rConfig config.ReportConfig - want ExitCode + name string + summ summary + status []checkStatus + staleExcls []config.Exclusion + rConfig config.ReportConfig + want ExitCode }{ { name: "critical", @@ -210,6 +212,73 @@ func TestWriter_calculateExitCode(t *testing.T) { }, want: ExitCodeCheckError, }, + { + name: "stale exclusions (warn)", + summ: summary{ + count: map[config.Severity]int{ + config.SeverityCritical: 0, + config.SeverityHigh: 0, + config.SeverityMedium: 1, + config.SeverityLow: 1, + config.SeverityInfo: 1, + }, + }, + status: []checkStatus{ + { + Checktype: "Checktype1", + Target: "Target1", + Status: "FINISHED", + }, + }, + staleExcls: []config.Exclusion{ + { + Summary: "Unused exclusion", + }, + }, + rConfig: config.ReportConfig{ + Severity: config.SeverityHigh, + Exclusions: []config.Exclusion{ + { + Summary: "Unused exclusion", + }, + }, + }, + want: 0, + }, + { + name: "stale exclusions (error)", + summ: summary{ + count: map[config.Severity]int{ + config.SeverityCritical: 0, + config.SeverityHigh: 0, + config.SeverityMedium: 1, + config.SeverityLow: 1, + config.SeverityInfo: 1, + }, + }, + status: []checkStatus{ + { + Checktype: "Checktype1", + Target: "Target1", + Status: "FINISHED", + }, + }, + staleExcls: []config.Exclusion{ + { + Summary: "Unused exclusion", + }, + }, + rConfig: config.ReportConfig{ + Severity: config.SeverityHigh, + ErrorOnStaleExclusions: true, + Exclusions: []config.Exclusion{ + { + Summary: "Unused exclusion", + }, + }, + }, + want: ExitCodeStaleExclusions, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -217,7 +286,7 @@ func TestWriter_calculateExitCode(t *testing.T) { if err != nil { t.Fatalf("unable to create a report writer: %v", err) } - got := w.calculateExitCode(tt.summ, tt.status) + got := w.calculateExitCode(tt.summ, tt.status, tt.staleExcls) if got != tt.want { t.Errorf("unexpected exit code: got: %v, want: %v", got, tt.want) } @@ -315,8 +384,8 @@ func TestWriter_parseReport(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: nil, }, { CheckData: vreport.CheckData{ @@ -326,8 +395,8 @@ func TestWriter_parseReport(t *testing.T) { Summary: "Vulnerability Summary 2", Score: 6.7, }, - Severity: config.SeverityMedium, - excluded: false, + Severity: config.SeverityMedium, + matchedExclusions: nil, }, }, wantNilErr: true, @@ -374,8 +443,8 @@ func TestWriter_parseReport(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: nil, }, { CheckData: vreport.CheckData{ @@ -385,8 +454,46 @@ func TestWriter_parseReport(t *testing.T) { Summary: "Vulnerability Summary 2", Score: 6.7, }, - Severity: config.SeverityMedium, - excluded: true, + Severity: config.SeverityMedium, + matchedExclusions: []int{0}, + }, + }, + wantNilErr: true, + }, + { + name: "vulnerability excluded by all the exclusions", + report: map[string]vreport.Report{ + "CheckID1": { + CheckData: vreport.CheckData{ + CheckID: "CheckID1", + }, + ResultData: vreport.ResultData{ + Vulnerabilities: []vreport.Vulnerability{ + { + Summary: "Vulnerability Summary 1", + AffectedResource: "Affected Resource 1", + }, + }, + }, + }, + }, + rConfig: config.ReportConfig{ + Exclusions: []config.Exclusion{ + {Summary: "Summary 1"}, + {Resource: "Affected Resource 1"}, + }, + }, + want: []vulnerability{ + { + CheckData: vreport.CheckData{ + CheckID: "CheckID1", + }, + Vulnerability: vreport.Vulnerability{ + Summary: "Vulnerability Summary 1", + AffectedResource: "Affected Resource 1", + }, + Severity: config.SeverityInfo, + matchedExclusions: []int{0, 1}, }, }, wantNilErr: true, @@ -413,13 +520,13 @@ func TestWriter_parseReport(t *testing.T) { } } -func TestWriter_isExcluded(t *testing.T) { +func TestWriter_matchExclusions(t *testing.T) { tests := []struct { name string vulnerability vreport.Vulnerability target string rConfig config.ReportConfig - want bool + want []int wantNilErr bool }{ { @@ -432,7 +539,7 @@ func TestWriter_isExcluded(t *testing.T) { rConfig: config.ReportConfig{ Exclusions: []config.Exclusion{}, }, - want: false, + want: []int{}, wantNilErr: true, }, { @@ -450,7 +557,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -468,7 +575,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: false, + want: []int{}, wantNilErr: true, }, { @@ -486,7 +593,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -504,7 +611,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -522,7 +629,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -539,7 +646,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -561,7 +668,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -583,7 +690,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -606,7 +713,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -629,7 +736,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: false, + want: []int{}, wantNilErr: true, }, { @@ -648,7 +755,7 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: true, + want: []int{0}, wantNilErr: true, }, { @@ -667,7 +774,24 @@ func TestWriter_isExcluded(t *testing.T) { }, }, }, - want: false, + want: []int{}, + wantNilErr: true, + }, + { + name: "match more than an exclusion", + vulnerability: vreport.Vulnerability{ + Summary: "Vulnerability Summary 1", + Score: 6.7, + AffectedResource: "Resource 1", + }, + target: ".", + rConfig: config.ReportConfig{ + Exclusions: []config.Exclusion{ + {Summary: "Summary 1"}, + {Resource: "Resource 1"}, + }, + }, + want: []int{0, 1}, wantNilErr: true, }, } @@ -683,11 +807,11 @@ func TestWriter_isExcluded(t *testing.T) { if err != nil { t.Fatalf("unable to create a report writer: %v", err) } - got, err := w.isExcluded(tt.vulnerability, tt.target) + got, err := w.matchExclusions(tt.vulnerability, tt.target) if (err == nil) != tt.wantNilErr { t.Errorf("unexpected error value: %v", err) } - if got != tt.want { + if !slices.Equal(tt.want, got) { t.Errorf("unexpected excluded value: got: %v, want: %v", got, tt.want) } }) @@ -708,71 +832,71 @@ func TestMkSummary(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityCritical, - excluded: false, + Severity: config.SeverityCritical, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 2", }, - Severity: config.SeverityCritical, - excluded: true, + Severity: config.SeverityCritical, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 3", }, - Severity: config.SeverityHigh, - excluded: false, + Severity: config.SeverityHigh, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 4", }, - Severity: config.SeverityHigh, - excluded: true, + Severity: config.SeverityHigh, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 5", }, - Severity: config.SeverityMedium, - excluded: false, + Severity: config.SeverityMedium, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 6", }, - Severity: config.SeverityMedium, - excluded: true, + Severity: config.SeverityMedium, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 7", }, - Severity: config.SeverityLow, - excluded: false, + Severity: config.SeverityLow, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 8", }, - Severity: config.SeverityLow, - excluded: true, + Severity: config.SeverityLow, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 9", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 10", }, - Severity: config.SeverityInfo, - excluded: true, + Severity: config.SeverityInfo, + matchedExclusions: []int{0}, }, }, want: summary{ @@ -794,8 +918,8 @@ func TestMkSummary(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: 7, - excluded: false, + Severity: 7, + matchedExclusions: []int{}, }, }, want: summary{ @@ -925,71 +1049,71 @@ func TestWriter_filterVulns(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityCritical, - excluded: false, + Severity: config.SeverityCritical, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 2", }, - Severity: config.SeverityCritical, - excluded: true, + Severity: config.SeverityCritical, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 3", }, - Severity: config.SeverityHigh, - excluded: false, + Severity: config.SeverityHigh, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 4", }, - Severity: config.SeverityHigh, - excluded: true, + Severity: config.SeverityHigh, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 5", }, - Severity: config.SeverityMedium, - excluded: false, + Severity: config.SeverityMedium, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 6", }, - Severity: config.SeverityMedium, - excluded: true, + Severity: config.SeverityMedium, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 7", }, - Severity: config.SeverityLow, - excluded: false, + Severity: config.SeverityLow, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 8", }, - Severity: config.SeverityLow, - excluded: true, + Severity: config.SeverityLow, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 9", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 10", }, - Severity: config.SeverityInfo, - excluded: true, + Severity: config.SeverityInfo, + matchedExclusions: []int{0}, }, }, rConfig: config.ReportConfig{ @@ -1000,36 +1124,36 @@ func TestWriter_filterVulns(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityCritical, - excluded: false, + Severity: config.SeverityCritical, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 3", }, - Severity: config.SeverityHigh, - excluded: false, + Severity: config.SeverityHigh, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 5", }, - Severity: config.SeverityMedium, - excluded: false, + Severity: config.SeverityMedium, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 7", }, - Severity: config.SeverityLow, - excluded: false, + Severity: config.SeverityLow, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 9", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: []int{}, }, }, }, @@ -1040,71 +1164,71 @@ func TestWriter_filterVulns(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityCritical, - excluded: false, + Severity: config.SeverityCritical, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 2", }, - Severity: config.SeverityCritical, - excluded: true, + Severity: config.SeverityCritical, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 3", }, - Severity: config.SeverityHigh, - excluded: false, + Severity: config.SeverityHigh, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 4", }, - Severity: config.SeverityHigh, - excluded: true, + Severity: config.SeverityHigh, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 5", }, - Severity: config.SeverityMedium, - excluded: false, + Severity: config.SeverityMedium, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 6", }, - Severity: config.SeverityMedium, - excluded: true, + Severity: config.SeverityMedium, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 7", }, - Severity: config.SeverityLow, - excluded: false, + Severity: config.SeverityLow, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 8", }, - Severity: config.SeverityLow, - excluded: true, + Severity: config.SeverityLow, + matchedExclusions: []int{0}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 9", }, - Severity: config.SeverityInfo, - excluded: false, + Severity: config.SeverityInfo, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 10", }, - Severity: config.SeverityInfo, - excluded: true, + Severity: config.SeverityInfo, + matchedExclusions: []int{0}, }, }, rConfig: config.ReportConfig{ @@ -1115,15 +1239,15 @@ func TestWriter_filterVulns(t *testing.T) { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 1", }, - Severity: config.SeverityCritical, - excluded: false, + Severity: config.SeverityCritical, + matchedExclusions: []int{}, }, { Vulnerability: vreport.Vulnerability{ Summary: "Vulnerability Summary 3", }, - Severity: config.SeverityHigh, - excluded: false, + Severity: config.SeverityHigh, + matchedExclusions: []int{}, }, }, }, @@ -1486,6 +1610,62 @@ func TestNewWriter_OutputFile(t *testing.T) { } } +func TestWriter_getStaleExclusions(t *testing.T) { + tests := []struct { + name string + exclusions []config.Exclusion + vulns []vulnerability + want []config.Exclusion + }{ + { + name: "without stale exclusions", + exclusions: []config.Exclusion{ + {Summary: "Summary 1"}, + {Resource: "Resource 1"}, + }, + vulns: []vulnerability{ + {matchedExclusions: []int{0, 1}}, + }, + want: []config.Exclusion{}, + }, + { + name: "matched all exclusion in different vulnerabilities", + exclusions: []config.Exclusion{ + {Summary: "Summary 1"}, + {Resource: "Resource 2"}, + }, + vulns: []vulnerability{ + {matchedExclusions: []int{0, 1}}, + {matchedExclusions: []int{0, 1}}, + }, + want: []config.Exclusion{}, + }, + { + name: "one stale exclusions", + exclusions: []config.Exclusion{ + {Summary: "Summary 1"}, + {Resource: "Resource 1"}, + {Summary: "Stale Exclusion 1"}, + }, + vulns: []vulnerability{ + {matchedExclusions: []int{0, 1}}, + }, + want: []config.Exclusion{ + {Summary: "Stale Exclusion 1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + writer := Writer{ + exclusions: tt.exclusions, + } + if got := writer.getStaleExclusions(tt.vulns); !slices.Equal(tt.want, got) { + t.Errorf("unexpected list of stale vulnerabilities: got: %v, want: %v", got, tt.want) + } + }) + } +} func vulnLess(a, b vulnerability) bool { h := func(v vulnerability) string { return fmt.Sprintf("%#v", v)