Skip to content

Commit

Permalink
feat: improve template table output
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <kzantow@gmail.com>
  • Loading branch information
kzantow committed Feb 14, 2024
1 parent 5327933 commit ac2d89b
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 51 deletions.
120 changes: 117 additions & 3 deletions grype/presenter/template/presenter.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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
}
70 changes: 22 additions & 48 deletions templates/table.tmpl
Original file line number Diff line number Diff line change
@@ -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}}
{{- 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" .}}

0 comments on commit ac2d89b

Please sign in to comment.