Add default table presenter (#59)

* add default table presenter

* compress table output

* fix table presenter found-by to use only search key
This commit is contained in:
Alex Goodman 2020-07-25 11:38:08 -04:00 committed by GitHub
parent ac36cd9b3d
commit 4220fc60a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 210 additions and 4 deletions

View file

@ -104,7 +104,7 @@ check-licenses:
unit: ## Run unit tests (with coverage)
$(call title,Running unit tests)
mkdir -p $(RESULTSDIR)
go test -v -coverprofile $(COVER_REPORT) ./...
go test -coverprofile $(COVER_REPORT) ./...
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
@echo "Coverage: $$(cat $(COVER_TOTAL))"
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi

View file

@ -64,7 +64,7 @@ func initAppConfig() {
}
func initLogging() {
config := logger.LogConfig{
cfg := logger.LogConfig{
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
EnableFile: appConfig.Log.FileLocation != "",
Level: appConfig.Log.LevelOpt,
@ -72,7 +72,7 @@ func initLogging() {
FileLocation: appConfig.Log.FileLocation,
}
logWrapper := logger.NewZapLogger(config)
logWrapper := logger.NewZapLogger(cfg)
log = logWrapper.Logger
grype.SetLogger(logWrapper)
syft.SetLogger(logWrapper)

View file

@ -70,7 +70,7 @@ func init() {
// output & formatting options
flag = "output"
rootCmd.Flags().StringP(
flag, "o", presenter.JSONPresenter.String(),
flag, "o", presenter.TablePresenter.String(),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {

1
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.4
github.com/sergi/go-diff v1.1.0
github.com/spf13/afero v1.3.2
github.com/spf13/cobra v1.0.0

4
go.sum
View file

@ -559,6 +559,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
@ -601,6 +603,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View file

@ -5,15 +5,18 @@ import "strings"
const (
UnknownPresenter Option = iota
JSONPresenter
TablePresenter
)
var optionStr = []string{
"UnknownPresenter",
"json",
"table",
}
var Options = []Option{
JSONPresenter,
TablePresenter,
}
type Option int
@ -22,6 +25,8 @@ func ParseOption(userStr string) Option {
switch strings.ToLower(userStr) {
case strings.ToLower(JSONPresenter.String()):
return JSONPresenter
case strings.ToLower(TablePresenter.String()):
return TablePresenter
default:
return UnknownPresenter
}

View file

@ -4,6 +4,7 @@ import (
"io"
"github.com/anchore/grype/grype/presenter/json"
"github.com/anchore/grype/grype/presenter/table"
"github.com/anchore/grype/grype/result"
"github.com/anchore/syft/syft/pkg"
)
@ -18,6 +19,8 @@ func GetPresenter(option Option) Presenter {
switch option {
case JSONPresenter:
return json.NewPresenter()
case TablePresenter:
return table.NewPresenter()
default:
return nil
}

View file

@ -0,0 +1,78 @@
package table
import (
"io"
"sort"
"github.com/anchore/grype/grype/result"
"github.com/anchore/syft/syft/pkg"
"github.com/olekukonko/tablewriter"
)
// Presenter is a generic struct for holding fields needed for reporting
type Presenter struct{}
// NewPresenter is a *Presenter constructor
func NewPresenter() *Presenter {
return &Presenter{}
}
// Present creates a JSON-based reporting
func (pres *Presenter) Present(output io.Writer, catalog *pkg.Catalog, results result.Result) error {
rows := make([][]string, 0)
columns := []string{"Name", "Installed", "Vulnerability", "Found-By"}
for p := range results.Enumerate() {
row := []string{
p.Package.Name,
p.Package.Version,
p.Vulnerability.ID,
p.SearchKey,
}
rows = append(rows, row)
}
if len(rows) == 0 {
_, err := io.WriteString(output, "No vulnerabilities found")
return err
}
// sort by name, version, then type
sort.SliceStable(rows, func(i, j int) bool {
for col := 0; col < len(columns); col++ {
if rows[i][0] != rows[j][0] {
return rows[i][col] < rows[j][col]
}
}
return false
})
table := tablewriter.NewWriter(output)
table.SetHeader(columns)
table.SetAutoWrapText(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
// these options allow for a more greppable table
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetAutoFormatHeaders(true)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
// these options allow for a more human-readable (but not greppable) table
//table.SetRowLine(true)
//table.SetAutoMergeCells(true)
//table.SetCenterSeparator("·") // + ┼ ╎ ┆ ┊ · •
//table.SetColumnSeparator("│")
//table.SetRowSeparator("─")
table.AppendBulk(rows)
table.Render()
return nil
}

View file

@ -0,0 +1,111 @@
package table
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/result"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestTablePresenter(t *testing.T) {
pres := NewPresenter()
var buffer bytes.Buffer
var pkg1 = pkg.Package{
Name: "package-1",
Version: "1.0.1",
Type: pkg.DebPkg,
}
var pkg2 = pkg.Package{
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
}
var match1 = match.Match{
Type: match.ExactDirectMatch,
Vulnerability: vulnerability.Vulnerability{ID: "CVE-1999-0001"},
Package: &pkg1,
Matcher: match.DpkgMatcher,
}
var match2 = match.Match{
Type: match.ExactIndirectMatch,
Vulnerability: vulnerability.Vulnerability{ID: "CVE-1999-0002"},
Package: &pkg2,
Matcher: match.DpkgMatcher,
SearchKey: "a search key...",
}
results := result.NewResult()
results.Add(&pkg1, match1, match2)
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg1)
catalog.Add(pkg2)
// TODO: add a constructor for a match.Match when the data is better shaped
// run presenter
err := pres.Present(&buffer, catalog, results)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
// TODO: add me back in when there is a JSON schema
// validateAgainstV1Schema(t, string(actual))
}
func TestEmptyTablePresenter(t *testing.T) {
// Expected to have no output
pres := NewPresenter()
var buffer bytes.Buffer
results := result.NewResult()
catalog := pkg.NewCatalog()
// run presenter
err := pres.Present(&buffer, catalog, results)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View file

@ -0,0 +1 @@
No vulnerabilities found

View file

@ -0,0 +1,3 @@
NAME INSTALLED VULNERABILITY FOUND-BY
package-1 1.0.1 CVE-1999-0001
package-2 2.0.1 CVE-1999-0002 a search key...