From e3a9e4f9b7d98e476ec6f1be8ff47f81bbdb0b1b Mon Sep 17 00:00:00 2001 From: Roi Martin Date: Mon, 23 Oct 2023 16:51:09 +0200 Subject: [PATCH] internal/report: simplify human-readable printer This commit introduces the following changes: - Move template parsing to program initialization. So, errors in the template can be caught just after running the command without having to wait until the scan finishes. - Merge the summary and vulnerability templates in a set of nested templates. - Refine output. --- internal/report/humanprinter.go | 99 ++++++----------- internal/report/humanprinter_test.go | 6 +- internal/report/templates/human.tmpl | 135 +++++++++++++++++++++++ internal/report/templates/humansum.tmpl | 19 ---- internal/report/templates/humanvuln.tmpl | 67 ----------- 5 files changed, 171 insertions(+), 155 deletions(-) create mode 100644 internal/report/templates/human.tmpl delete mode 100644 internal/report/templates/humansum.tmpl delete mode 100644 internal/report/templates/humanvuln.tmpl diff --git a/internal/report/humanprinter.go b/internal/report/humanprinter.go index 157edd4..3945b7f 100644 --- a/internal/report/humanprinter.go +++ b/internal/report/humanprinter.go @@ -17,89 +17,56 @@ import ( // humanPrinter represents a human-readable report printer. type humanPrinter struct{} -// colorFuncs stores common function to colorize texts. -var commonFuncs = template.FuncMap{ - "magenta": color.New(color.FgMagenta).SprintfFunc(), - "red": color.New(color.FgRed).SprintfFunc(), - "yellow": color.New(color.FgYellow).SprintfFunc(), - "cyan": color.New(color.FgCyan).SprintfFunc(), - "bold": color.New(color.Bold).SprintfFunc(), - "upper": strings.ToUpper, -} +var ( + //go:embed templates/human.tmpl + humanReport string -// Print renders the scan results in human-readable format. -func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, sum summary) error { - if err := printSummary(w, sum); err != nil { - return fmt.Errorf("print summary: %w", err) - } - if len(vulns) > 0 { - _, err := fmt.Fprint(w, "\nVulnerabilities details:\n") - if err != nil { - return fmt.Errorf("print vulnerability details: %w", err) - } - } - for _, v := range vulns { - if err := printVulnerability(w, v); err != nil { - return fmt.Errorf("print vulnerability: %w", err) - } + // humanTmplFuncs stores the functions called from the + // template used to render the human-readable report. + humanTmplFuncs = template.FuncMap{ + "magenta": color.New(color.FgMagenta).SprintfFunc(), + "red": color.New(color.FgRed).SprintfFunc(), + "yellow": color.New(color.FgYellow).SprintfFunc(), + "cyan": color.New(color.FgCyan).SprintfFunc(), + "bold": color.New(color.Bold).SprintfFunc(), + "underline": color.New(color.Underline).SprintfFunc(), + "upper": strings.ToUpper, + "trim": strings.TrimSpace, } - return nil -} -//go:embed templates/humansum.tmpl -var humanSummary string + // humanTmpl is the template used to render the human-readable + // report. + humanTmpl = template.Must(template.New("").Funcs(humanTmplFuncs).Parse(humanReport)) +) -// printSummary renders the summary table with the vulnerability stats. -func printSummary(writer io.Writer, sum summary) error { - var total int +// Print renders the scan results in a human-readable format. +func (prn humanPrinter) Print(w io.Writer, vulns []vulnerability, sum summary) error { // count the total non-excluded vulnerabilities found. + var total int for _, ss := range sum.count { total += ss } - type severityCount struct { - Name string - Count int - } - - var sevCounts []severityCount - for sev := config.SeverityCritical; sev >= config.SeverityInfo; sev-- { - sevCount := severityCount{ - Name: sev.String(), - Count: sum.count[sev], - } - sevCounts = append(sevCounts, sevCount) + stats := make(map[string]int) + for s := config.SeverityCritical; s >= config.SeverityInfo; s-- { + stats[s.String()] = sum.count[s] } data := struct { - SevCounts []severityCount - Total int - Excluded int + Stats map[string]int + Total int + Excluded int + Vulns []vulnerability }{ - SevCounts: sevCounts, - Total: total, - Excluded: sum.excluded, + Stats: stats, + Total: total, + Excluded: sum.excluded, + Vulns: vulns, } - sumTmpl := template.New("summary") - sumTmpl = sumTmpl.Funcs(commonFuncs) - sumTmpl = template.Must(sumTmpl.Parse(humanSummary)) - if err := sumTmpl.Execute(writer, data); err != nil { + if err := humanTmpl.Execute(w, data); err != nil { return fmt.Errorf("execute template summary: %w", err) } - return nil -} -//go:embed templates/humanvuln.tmpl -var humanVuln string - -// printVulnerability renders a vulnerability in a human-readable format. -func printVulnerability(writer io.Writer, v vulnerability) error { - vulnTmpl := template.New("vulnerability") - vulnTmpl = vulnTmpl.Funcs(commonFuncs) - vulnTmpl = template.Must(vulnTmpl.Parse(humanVuln)) - if err := vulnTmpl.Execute(writer, v); err != nil { - return fmt.Errorf("execute template vulnerability: %w", err) - } return nil } diff --git a/internal/report/humanprinter_test.go b/internal/report/humanprinter_test.go index 8244c9f..4467b75 100644 --- a/internal/report/humanprinter_test.go +++ b/internal/report/humanprinter_test.go @@ -155,9 +155,9 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { excluded: 3, }, want: []string{ - "Summary of the last scan:", + "SUMMARY", "Number of excluded vulnerabilities not included in the summary table: 3", - "Vulnerabilities details:", + "VULNERABILITIES", "Vulnerability Summary 1", }, }, @@ -165,7 +165,7 @@ func TestUserFriendlyPrinter_Print(t *testing.T) { name: "No vulnerabilities", vulnerabilities: nil, want: []string{ - "No vulnerabilities found during the Lava scan.", + "No vulnerabilities found during the scan.", }, }, } diff --git a/internal/report/templates/human.tmpl b/internal/report/templates/human.tmpl new file mode 100644 index 0000000..a08b70e --- /dev/null +++ b/internal/report/templates/human.tmpl @@ -0,0 +1,135 @@ +{{- /* report is the template used to render the full scan report. */ -}} +{{- define "report" -}} +{{"SUMMARY" | bold | underline}} +{{if .Total}} +{{template "summary" .}} +{{else}} +No vulnerabilities found during the scan. +{{end}} +{{- if .Vulns}} +{{template "vulns" . -}} +{{end -}} +{{- end -}} + + +{{- /* summary is the template used to render the scan summary. */ -}} +{{- define "summary" -}} +{{"CRITICAL" | bold | magenta}}: {{index .Stats "critical"}} +{{"HIGH" | bold | red}}: {{index .Stats "high"}} +{{"MEDIUM" | bold | yellow}}: {{index .Stats "medium"}} +{{"LOW" | bold | cyan}}: {{index .Stats "low"}} +{{"INFO" | bold}}: {{index .Stats "info"}} + +Number of excluded vulnerabilities not included in the summary table: {{.Excluded}} +{{- end -}} + + +{{- /* vulns is the template used to render all the vulnerability reports. */ -}} +{{- define "vulns" -}} +{{"VULNERABILITIES" | bold | underline}} +{{range .Vulns}} +{{template "vuln" . -}} +{{end}} +{{- end -}} + + +{{- /* vuln is the template used to render one vulnerability report */ -}} +{{- define "vuln" -}} +{{template "vulnTitle" .}} + +{{"TARGET" | bold}} +{{.CheckData.Target | trim}} +{{""}} + +{{- $affectedResource:= .AffectedResourceString -}} +{{- if not $affectedResource -}} + {{- $affectedResource = .AffectedResource -}} +{{- end -}} +{{- if $affectedResource}} +{{"AFFECTED RESOURCE" | bold}} +{{$affectedResource | trim}} +{{end -}} + +{{- if .Description}} +{{"DESCRIPTION" | bold}} +{{.Description | trim}} +{{end -}} + +{{- if .Details}} +{{"DETAILS" | bold}} +{{.Details | trim}} +{{end -}} + +{{- if .ImpactDetails}} +{{"IMPACT" | bold}} +{{.ImpactDetails | trim}} +{{end -}} + +{{- if .Recommendations}} +{{template "vulnRecoms" .}} +{{end -}} + +{{- if .References}} +{{template "vulnRefs" .}} +{{end -}} + +{{- if .Resources}} +{{template "vulnRscs" .}} +{{end -}} +{{- end -}} + + +{{- /* vulnTitle is the template used to render the title of a vulnerability. */ -}} +{{- define "vulnTitle" -}} +{{- if eq .Severity.String "critical" -}} + {{printf "=== %v (%v) ===" (trim .Summary) (upper .Severity.String) | bold | magenta}} +{{- else if eq .Severity.String "high" -}} + {{printf "=== %v (%v) ===" (trim .Summary) (upper .Severity.String) | bold | red}} +{{- else if eq .Severity.String "medium" -}} + {{printf "=== %v (%v) ===" (trim .Summary) (upper .Severity.String) | bold | yellow}} +{{- else if eq .Severity.String "low" -}} + {{printf "=== %v (%v) ===" (trim .Summary) (upper .Severity.String) | bold | cyan}} +{{- else -}} + {{printf "=== %v (%v) ===" (trim .Summary) (upper .Severity.String) | bold}} +{{- end -}} +{{- end -}} + + +{{- /* vulnRecoms is the template used to render the recommendations to fix a vulnerability. */ -}} +{{- define "vulnRecoms" -}} +{{"RECOMMENDATIONS" | bold}} +{{- range .Recommendations}} +- {{. | trim -}} +{{end}} +{{- end -}} + + +{{- /* vulnRefs is the template used to render a list of references with more details about the vulnerability. */ -}} +{{- define "vulnRefs" -}} +{{"REFERENCES" | bold}} +{{- range .References}} +- {{. | trim -}} +{{end}} +{{- end -}} + + +{{- /* vulnRscs is the template used to render the list of affected resources. */ -}} +{{- define "vulnRscs" -}} +{{"RESOURCES" | bold}} +{{- range $resource := .Resources}} +{{template "vulnRsc" . -}} +{{end}} +{{- end -}} + + +{{- /* vulnRsc is the template used to render the details of a single resource. */ -}} +{{- define "vulnRsc" -}} +{{- $rsc := . -}} +- {{$rsc.Name | bold}}: +{{- range $row := $rsc.Rows}}{{range $header := $rsc.Header}} + {{$header | trim | bold}}: {{index $row $header | trim -}} +{{end}}{{end}} +{{- end -}} + + +{{- template "report" . -}} diff --git a/internal/report/templates/humansum.tmpl b/internal/report/templates/humansum.tmpl deleted file mode 100644 index a10df97..0000000 --- a/internal/report/templates/humansum.tmpl +++ /dev/null @@ -1,19 +0,0 @@ -Summary of the last scan: -{{if not .Total}} - No vulnerabilities found during the Lava scan. -{{else}} - {{- range .SevCounts -}} - {{- if eq .Name "critical" -}} - {{.Name | upper | bold | magenta}}: {{.Count}}{{"\n"}} - {{- else if eq .Name "high" -}} - {{.Name | upper | bold | red}}: {{.Count}}{{"\n"}} - {{- else if eq .Name "medium" -}} - {{.Name | upper | bold | yellow}}: {{.Count}}{{"\n"}} - {{- else if eq .Name "low" -}} - {{- .Name | upper | bold | cyan}}: {{.Count}}{{"\n"}} - {{- else -}} - {{.Name | upper | bold}}: {{.Count}}{{"\n"}} - {{- end -}} - {{- end -}} - {{"\n"}}Number of excluded vulnerabilities not included in the summary table: {{.Excluded}} -{{end}} diff --git a/internal/report/templates/humanvuln.tmpl b/internal/report/templates/humanvuln.tmpl deleted file mode 100644 index 292031d..0000000 --- a/internal/report/templates/humanvuln.tmpl +++ /dev/null @@ -1,67 +0,0 @@ -{{- if eq .Severity.String "critical" -}} - {{"\n"}}{{"=====================================================" | bold | magenta -}} - {{"\n"}}{{.Severity.String | upper | bold | magenta -}} - {{"\n"}}{{"=====================================================" | bold | magenta -}} -{{- else if eq .Severity.String "high" -}} - {{"\n"}}{{"=====================================================" | bold | red -}} - {{"\n"}}{{- .Severity.String | upper | bold | red -}} - {{"\n"}}{{"=====================================================" | bold | red -}} -{{- else if eq .Severity.String "medium" -}} - {{"\n"}}{{"=====================================================" | bold | yellow -}} - {{"\n"}}{{.Severity.String | upper | bold | yellow -}} - {{"\n"}}{{"=====================================================" | bold | yellow -}} -{{- else if eq .Severity.String "low" -}} - {{"\n"}}{{"=====================================================" | bold | cyan -}} - {{"\n"}}{{.Severity.String | upper | bold | cyan -}} - {{"\n"}}{{"=====================================================" | bold | cyan -}} -{{- else -}} - {{"\n"}}{{"=====================================================" | bold -}} - {{"\n"}}{{.Severity.String | upper | bold -}} - {{"\n"}}{{"=====================================================" | bold -}} -{{- end -}} -{{"\n"}}{{"TARGET:" | bold}} -{{.CheckData.Target -}} -{{$affectedResource:= .AffectedResourceString -}} -{{- if not $affectedResource -}} - {{$affectedResource = .AffectedResource -}} -{{- end -}} -{{- if $affectedResource -}} - {{"\n"}}{{"\n"}}{{"AFFECTED RESOURCE:" | bold -}} - {{"\n"}}{{$affectedResource}} -{{- end -}} -{{"\n"}}{{"\n"}}{{"SUMMARY:" | bold -}} -{{"\n"}}{{.Summary -}} -{{"\n"}}{{"\n"}}{{"DESCRIPTION:" | bold -}} -{{"\n"}}{{.Description}} -{{- if .Details -}} - {{"\n"}}{{"\n"}}{{"DETAILS:" | bold -}} - {{"\n"}}{{.Details}} -{{- end -}} -{{- if .ImpactDetails -}} - {{"\n"}}{{"\n"}}{{"IMPACT:" | bold -}} - {{"\n"}}{{ .ImpactDetails}} -{{- end -}} -{{- if gt (len .Recommendations) 0 -}} - {{"\n"}}{{"\n"}}{{"RECOMMENDATIONS:" | bold -}} - {{- range .Recommendations -}} - {{"\n"}}- {{.}} - {{- end -}} -{{- end -}} -{{- if gt (len .References) 0 -}} - {{"\n"}}{{"\n"}}{{"REFERENCES:" | bold -}} - {{- range .References -}} - {{"\n"}}- {{.}} - {{- end -}} -{{- end -}} -{{- if gt (len .Resources) 0 -}} - {{- range $resource := .Resources -}} - {{"\n"}}{{"\n"}}{{.Name | bold -}}: - {{- $headers := .Header -}} - {{- range $row := .Rows -}} - {{- range $header := $headers -}} - {{"\n"}}{{$header | bold}}: {{index $row $header -}} - {{- end -}} - {{"\n"}} - {{- end -}} - {{- end -}} -{{- end -}}