From ac2d89b7d81115ccd227a70b3ccbe9a81fdcec3a Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 14 Feb 2024 16:26:37 -0500 Subject: [PATCH 1/2] feat: improve template table output Signed-off-by: Keith Zantow --- grype/presenter/template/presenter.go | 120 +++++++++++++++++++++++++- templates/table.tmpl | 70 +++++---------- 2 files changed, 139 insertions(+), 51 deletions(-) diff --git a/grype/presenter/template/presenter.go b/grype/presenter/template/presenter.go index d6fe7a31c78..3337401085e 100644 --- a/grype/presenter/template/presenter.go +++ b/grype/presenter/template/presenter.go @@ -1,15 +1,19 @@ package template import ( + "bytes" "fmt" "io" "os" "reflect" + "regexp" "sort" + "strings" "text/template" "github.com/Masterminds/sprig/v3" "github.com/mitchellh/go-homedir" + "github.com/olekukonko/tablewriter" "github.com/anchore/clio" "github.com/anchore/grype/grype/match" @@ -59,7 +63,9 @@ func (pres *Presenter) Present(output io.Writer) error { } templateName := expandedPathToTemplateFile - tmpl, err := template.New(templateName).Funcs(FuncMap).Parse(string(templateContents)) + var tmpl *template.Template + tmpl = template.New(templateName).Funcs(FuncMap(&tmpl)) + tmpl, err = tmpl.Parse(string(templateContents)) if err != nil { return fmt.Errorf("unable to parse template: %w", err) } @@ -79,7 +85,7 @@ func (pres *Presenter) Present(output io.Writer) error { } // FuncMap is a function that returns template.FuncMap with custom functions available to template authors. -var FuncMap = func() template.FuncMap { +func FuncMap(tpl **template.Template) template.FuncMap { f := sprig.HermeticTxtFuncMap() f["getLastIndex"] = func(collection interface{}) int { if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice { @@ -97,5 +103,113 @@ var FuncMap = func() template.FuncMap { sort.Sort(models.MatchSort(matches)) return matches } + f["csvToTable"] = csvToTable(tpl) + f["inline"] = inlineLines(tpl) + f["uniqueLines"] = uniqueLines(tpl) return f -}() +} + +// csvToTable removes any whitespace-only lines, and renders a table based csv from the rendered template +func csvToTable(tpl **template.Template) func(templateName string, data any) (string, error) { + return func(templateName string, data any) (string, error) { + in, err := evalTemplate(tpl, templateName, data) + if err != nil { + return "", err + } + lines := strings.Split(in, "\n") + + // remove blank lines + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if len(line) == 0 { + lines = append(lines[:i], lines[i+1:]...) + i-- + continue + } + lines[i] = line + } + + header := strings.TrimSpace(lines[0]) + columns := strings.Split(header, ",") + + out := bytes.Buffer{} + + table := tablewriter.NewWriter(&out) + table.SetHeader(columns) + table.SetAutoWrapText(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetAutoFormatHeaders(true) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetTablePadding(" ") + table.SetNoWhiteSpace(true) + + for _, line := range lines[1:] { + line = strings.TrimSpace(line) + row := strings.Split(line, ",") + for i := range row { + row[i] = strings.TrimSpace(row[i]) + } + table.Append(row) + } + + table.Render() + + return out.String(), nil + } +} + +// inlineLines take a multi-line rendered template string and remove newlines +func inlineLines(tpl **template.Template) func(templateName string, data any) (string, error) { + return func(templateName string, data any) (string, error) { + text, err := evalTemplate(tpl, templateName, data) + if err != nil { + return "", err + } + text = regexp.MustCompile(`[\r\n]`).ReplaceAllString(text, "") + return text, nil + } +} + +// uniqueLines remove any duplicate lines, leaving only one copy from a rendered template +func uniqueLines(tpl **template.Template) func(templateName string, data any) (string, error) { + return func(templateName string, data any) (string, error) { + text, err := evalTemplate(tpl, templateName, data) + if err != nil { + return "", err + } + allLines := strings.Split(text, "\n") + out := bytes.Buffer{} + nextLine: + for i := 0; i < len(allLines); i++ { + line := allLines[i] + for j := 0; j < i; j++ { + if allLines[j] == line { + continue nextLine + } + } + if out.Len() > 0 { + out.WriteRune('\n') + } + out.WriteString(line) + } + if strings.HasSuffix(text, "\n") { + out.WriteRune('\n') + } + return out.String(), nil + } +} + +func evalTemplate(tpl **template.Template, templateName string, data any) (string, error) { + out := bytes.Buffer{} + err := (*tpl).ExecuteTemplate(&out, templateName, data) + if err != nil { + return "", err + } + return out.String(), nil +} diff --git a/templates/table.tmpl b/templates/table.tmpl index 519cfef03d8..304644f31bd 100644 --- a/templates/table.tmpl +++ b/templates/table.tmpl @@ -1,48 +1,22 @@ -{{- $name_length := 4}} -{{- $installed_length := 9}} -{{- $fixed_in_length := 8}} -{{- $type_length := 4}} -{{- $vulnerability_length := 13}} -{{- $severity_length := 8}} -{{- range .Matches}} -{{- $temp_name_length := (len .Artifact.Name)}} -{{- $temp_installed_length := (len .Artifact.Version)}} -{{- $temp_fixed_in_length := (len (.Vulnerability.Fix.Versions | join " "))}} -{{- $temp_type_length := (len .Artifact.Type)}} -{{- $temp_vulnerability_length := (len .Vulnerability.ID)}} -{{- $temp_severity_length := (len .Vulnerability.Severity)}} -{{- if (lt $name_length $temp_name_length) }} -{{- $name_length = $temp_name_length}} -{{- end}} -{{- if (lt $installed_length $temp_installed_length) }} -{{- $installed_length = $temp_installed_length}} -{{- end}} -{{- if (lt $fixed_in_length $temp_fixed_in_length) }} -{{- $fixed_in_length = $temp_fixed_in_length}} -{{- end}} -{{- if (lt $type_length $temp_type_length) }} -{{- $type_length = $temp_type_length}} -{{- end}} -{{- if (lt $vulnerability_length $temp_vulnerability_length) }} -{{- $vulnerability_length = $temp_vulnerability_length}} -{{- end}} -{{- if (lt $severity_length $temp_severity_length) }} -{{- $severity_length = $temp_severity_length}} -{{- end}} -{{- end}} -{{- $name_length = add $name_length 2}} -{{- $pad_name := repeat (int $name_length) " "}} -{{- $installed_length = add $installed_length 2}} -{{- $pad_installed := repeat (int $installed_length) " "}} -{{- $fixed_in_length = add $fixed_in_length 2}} -{{- $pad_fixed_in := repeat (int $fixed_in_length) " "}} -{{- $type_length = add $type_length 2}} -{{- $pad_type := repeat (int $type_length) " "}} -{{- $vulnerability_length = add $vulnerability_length 2}} -{{- $pad_vulnerability := repeat (int $vulnerability_length) " "}} -{{- $severity_length = add $severity_length 2}} -{{- $pad_severity := repeat (int $severity_length) " "}} -{{cat "NAME" (substr 5 (int $name_length) $pad_name)}}{{cat "INSTALLED" (substr 10 (int $installed_length) $pad_installed)}}{{cat "FIXED-IN" (substr 9 (int $fixed_in_length) $pad_fixed_in)}}{{cat "TYPE" (substr 5 (int $type_length) $pad_type)}}{{cat "VULNERABILITY" (substr 14 (int $vulnerability_length) $pad_vulnerability)}}{{cat "SEVERITY" (substr 9 (int $severity_length) $pad_severity)}} -{{- range .Matches}} -{{cat .Artifact.Name (substr (int (add (len .Artifact.Name) 1)) (int $name_length) $pad_name)}}{{cat .Artifact.Version (substr (int (add (len .Artifact.Version) 1)) (int $installed_length) $pad_installed)}}{{cat (.Vulnerability.Fix.Versions | join " ") (substr (int (add (len (.Vulnerability.Fix.Versions | join " ")) 1)) (int $fixed_in_length) $pad_fixed_in)}}{{cat .Artifact.Type (substr (int (add (len .Artifact.Type) 1)) (int $type_length) $pad_type)}}{{cat .Vulnerability.ID (substr (int (add (len .Vulnerability.ID) 1)) (int $vulnerability_length) $pad_vulnerability)}}{{cat .Vulnerability.Severity (substr (int (add (len .Vulnerability.Severity) 1)) (int $severity_length) $pad_severity)}} -{{- end}} \ No newline at end of file +{{- define "line"}} + {{.Artifact.Name}} + ,{{.Artifact.Version}} + ,{{.Vulnerability.Fix.Versions | join " "}} + ,{{.Artifact.Type}} + ,{{.Vulnerability.ID}} + ,{{.Vulnerability.Severity}} + ,{{range .Artifact.Locations}}{{.RealPath}} {{end}} +{{end}} + +{{- define "matches"}} + {{range .Matches}} + {{inline "line" .}} + {{end}} +{{end}} + +{{- define "table"}} +Name, Version, Fixed-in, Type, Vulnerability, Severity,Location +{{uniqueLines "matches" .}} +{{end}} + +{{- csvToTable "table" .}} \ No newline at end of file From 437ce9f73959716510bcba0ddc4ccd9422d6aa09 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 16 Feb 2024 20:44:29 -0500 Subject: [PATCH 2/2] chore: better naming Signed-off-by: Keith Zantow --- grype/presenter/template/presenter.go | 18 +++++++++--------- templates/table.tmpl | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/grype/presenter/template/presenter.go b/grype/presenter/template/presenter.go index 3337401085e..6af4b89aeea 100644 --- a/grype/presenter/template/presenter.go +++ b/grype/presenter/template/presenter.go @@ -103,14 +103,14 @@ func FuncMap(tpl **template.Template) template.FuncMap { sort.Sort(models.MatchSort(matches)) return matches } - f["csvToTable"] = csvToTable(tpl) - f["inline"] = inlineLines(tpl) - f["uniqueLines"] = uniqueLines(tpl) + f["templateCsvToTable"] = templateCsvToTable(tpl) + f["templateRemoveNewlines"] = templateRemoveNewlines(tpl) + f["templateUniqueLines"] = templateUniqueLines(tpl) return f } -// csvToTable removes any whitespace-only lines, and renders a table based csv from the rendered template -func csvToTable(tpl **template.Template) func(templateName string, data any) (string, error) { +// templateCsvToTable removes any whitespace-only lines and renders a table based csv from the named template +func templateCsvToTable(tpl **template.Template) func(templateName string, data any) (string, error) { return func(templateName string, data any) (string, error) { in, err := evalTemplate(tpl, templateName, data) if err != nil { @@ -164,8 +164,8 @@ func csvToTable(tpl **template.Template) func(templateName string, data any) (st } } -// inlineLines take a multi-line rendered template string and remove newlines -func inlineLines(tpl **template.Template) func(templateName string, data any) (string, error) { +// templateRemoveNewlines remove all newlines from the rendered template +func templateRemoveNewlines(tpl **template.Template) func(templateName string, data any) (string, error) { return func(templateName string, data any) (string, error) { text, err := evalTemplate(tpl, templateName, data) if err != nil { @@ -176,8 +176,8 @@ func inlineLines(tpl **template.Template) func(templateName string, data any) (s } } -// uniqueLines remove any duplicate lines, leaving only one copy from a rendered template -func uniqueLines(tpl **template.Template) func(templateName string, data any) (string, error) { +// templateUniqueLines remove any duplicate lines from the rendered template +func templateUniqueLines(tpl **template.Template) func(templateName string, data any) (string, error) { return func(templateName string, data any) (string, error) { text, err := evalTemplate(tpl, templateName, data) if err != nil { diff --git a/templates/table.tmpl b/templates/table.tmpl index 304644f31bd..224aff09e5c 100644 --- a/templates/table.tmpl +++ b/templates/table.tmpl @@ -10,13 +10,13 @@ {{- define "matches"}} {{range .Matches}} - {{inline "line" .}} + {{templateRemoveNewlines "line" .}} {{end}} {{end}} {{- define "table"}} -Name, Version, Fixed-in, Type, Vulnerability, Severity,Location -{{uniqueLines "matches" .}} +Name, Version, Fixed-in, Type, Vulnerability, Severity, Location +{{templateUniqueLines "matches" .}} {{end}} -{{- csvToTable "table" .}} \ No newline at end of file +{{- templateCsvToTable "table" .}} \ No newline at end of file