diff --git a/grype/presenter/template/presenter.go b/grype/presenter/template/presenter.go index d6fe7a31c78..6af4b89aeea 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["templateCsvToTable"] = templateCsvToTable(tpl) + f["templateRemoveNewlines"] = templateRemoveNewlines(tpl) + f["templateUniqueLines"] = templateUniqueLines(tpl) return f -}() +} + +// 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 { + 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 + } +} + +// 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 { + return "", err + } + text = regexp.MustCompile(`[\r\n]`).ReplaceAllString(text, "") + return text, nil + } +} + +// 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 { + 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..224aff09e5c 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}} + {{templateRemoveNewlines "line" .}} + {{end}} +{{end}} + +{{- define "table"}} +Name, Version, Fixed-in, Type, Vulnerability, Severity, Location +{{templateUniqueLines "matches" .}} +{{end}} + +{{- templateCsvToTable "table" .}} \ No newline at end of file