mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
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:
parent
ac36cd9b3d
commit
4220fc60a7
11 changed files with 210 additions and 4 deletions
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
78
grype/presenter/table/presenter.go
Normal file
78
grype/presenter/table/presenter.go
Normal 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
|
||||
}
|
111
grype/presenter/table/presenter_test.go
Normal file
111
grype/presenter/table/presenter_test.go
Normal 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))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
No vulnerabilities found
|
|
@ -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...
|
Loading…
Reference in a new issue