Skip to content

Commit

Permalink
internal/report: detect unused exclusions
Browse files Browse the repository at this point in the history
  • Loading branch information
seilagamo committed Aug 23, 2024
1 parent f7e76af commit 6df5263
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 27 deletions.
4 changes: 4 additions & 0 deletions cmd/lava/internal/help/helpdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- error_on_stale_exclusions: set whether Lava exits with an exit
code when there are stale exclusions. If not specified, the
default value is false.
The sample below is a full report configuration:
Expand All @@ -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/'
error_on_stale_exclusions: true
The exclusion rules support the following filters:
Expand Down
1 change: 1 addition & 0 deletions cmd/lava/internal/scan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ that have been found.
- 1: Command error
- 2: Syntax error
- 3: Check error
- 4: There are stale exclusions
- 100: Informational vulnerabilities found
- 101: Low severity vulnerabilities found
- 102: Medium severity vulnerabilities found
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ type ReportConfig struct {
// instance, accepted risks, false positives, etc.
Exclusions []Exclusion `yaml:"exclusions"`

// ErrorOnStaleExclusions exits Lava with an exit code when there
// are stale exclusions.
ErrorOnStaleExclusions bool `yaml:"error_on_stale_exclusions"`

// 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.
Expand Down
38 changes: 37 additions & 1 deletion internal/report/human.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
{{- if .Vulns}}
{{template "vulns" . -}}
{{end -}}
{{- if not .AllExclMatched}}
{{template "staleExcls" . -}}
{{end -}}
{{- end -}}


Expand Down Expand Up @@ -158,10 +161,43 @@ 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 := .Exclusions}}
{{- if not $excl.Matched}}
{{- template "excl" . -}}
{{end -}}
{{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" . -}}
36 changes: 25 additions & 11 deletions internal/report/humanprinter.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ 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, exclusions []reportExclusion) error {
// count the total non-excluded vulnerabilities found.
var total int
for _, ss := range summ.count {
Expand All @@ -52,18 +53,31 @@ func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, summ summary,
stats[s.String()] = summ.count[s]
}

// check if there are exclusions not matched.
var allMatched = true
for _, e := range exclusions {
if !e.Matched {
allMatched = false
break
}
}

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
Exclusions []reportExclusion
}{
Stats: stats,
Total: total,
Excluded: summ.excluded,
Vulns: vulns,
Status: status,
Stats: stats,
Total: total,
Excluded: summ.excluded,
Vulns: vulns,
Status: status,
AllExclMatched: allMatched,
Exclusions: exclusions,
}

if err := humanTmpl.Execute(w, data); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/report/humanprinter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestUserFriendlyPrinter_Print(t *testing.T) {
vulnerabilities []vulnerability
summ summary
status []checkStatus
exclusions []reportExclusion
want []string
}{
{
Expand Down Expand Up @@ -193,7 +194,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.exclusions); err != nil {
t.Errorf("unexpected error value: %v", err)
}
text := buf.String()
Expand Down
2 changes: 1 addition & 1 deletion internal/report/jsonprinter.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
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, _ []reportExclusion) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(vulns); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/report/jsonprinter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
58 changes: 46 additions & 12 deletions internal/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"os"
"regexp"
"slices"
Expand All @@ -28,7 +29,13 @@ type Writer struct {
isStdout bool
minSeverity config.Severity
showSeverity config.Severity
exclusions []config.Exclusion
exclusions []reportExclusion
eose bool
}

type reportExclusion struct {
config.Exclusion
Matched bool
}

// timeNow is set by tests to mock the current time.
Expand Down Expand Up @@ -64,13 +71,19 @@ func NewWriter(cfg config.ReportConfig) (Writer, error) {
showSeverity = cfg.Severity
}

var exclusions []reportExclusion
for _, e := range cfg.Exclusions {
exclusions = append(exclusions, reportExclusion{Exclusion: e, Matched: false})
}

return Writer{
prn: prn,
w: w,
isStdout: isStdout,
minSeverity: cfg.Severity,
showSeverity: showSeverity,
exclusions: cfg.Exclusions,
exclusions: exclusions,
eose: cfg.ErrorOnStaleExclusions,
}, nil
}

Expand All @@ -96,7 +109,7 @@ func (writer Writer) Write(er engine.Report) (ExitCode, error) {
status := mkStatus(er)
exitCode := writer.calculateExitCode(summ, status)

if err = writer.prn.Print(writer.w, fvulns, summ, status); err != nil {
if err = writer.prn.Print(writer.w, fvulns, summ, status, writer.exclusions); err != nil {
return exitCode, fmt.Errorf("print report: %w", err)
}

Expand Down Expand Up @@ -140,8 +153,8 @@ func (writer Writer) parseReport(er engine.Report) ([]vulnerability, error) {

// 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 {
func (writer *Writer) isExcluded(v report.Vulnerability, target string) (bool, error) {
for i, excl := range writer.exclusions {
if !excl.ExpirationDate.IsZero() && excl.ExpirationDate.Before(timeNow()) {
continue
}
Expand Down Expand Up @@ -183,6 +196,8 @@ func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, er
continue
}
}

writer.exclusions[i].Matched = true
return true, nil
}
return false, nil
Expand Down Expand Up @@ -224,6 +239,24 @@ func (writer Writer) calculateExitCode(summ summary, status []checkStatus) ExitC
}
}

var notMatched bool
// Log all the not-matched exclusions.
for i, e := range writer.exclusions {
if !e.Matched {
notMatched = true
var loggerLevel func(msg string, args ...any)
if writer.eose {
loggerLevel = slog.Error
} else {
loggerLevel = slog.Warn
}
loggerLevel("The exclusion doesn't much with any finding", "index", i)
}
}
if writer.eose && notMatched {
return ExitCodeStaleExclusions
}

for sev := config.SeverityCritical; sev >= writer.minSeverity; sev-- {
if summ.count[sev] > 0 {
diff := sev - config.SeverityInfo
Expand All @@ -243,7 +276,7 @@ type vulnerability struct {

// 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, exclusions []reportExclusion) error
}

// scoreToSeverity converts a CVSS score into a [config.Severity].
Expand Down Expand Up @@ -324,10 +357,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
)
Loading

0 comments on commit 6df5263

Please sign in to comment.