Skip to content

Commit

Permalink
Colorize severity in table output
Browse files Browse the repository at this point in the history
- Create flag "--no-color" to allow disabling the color. By default its enabled.
- When "--no-color" not specified highlight severity in its color:
  - Critical -> Bold Red
  - High -> Red
  - Medium -> Yellow
  - Low -> Green
  - Negligible -> Blue
  - Note: Golang doesn't have all colors available. Also, doesn't seem to be able use hex codes properly.
- Add termenv to check if the terminal color profile supports colored output. If it doesn't default to noColor

Closes anchore#225

Signed-off-by: Shane Dell <shanedell100@gmail.com>
  • Loading branch information
shanedell committed Jun 12, 2023
1 parent c47304b commit 27962c1
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 14 deletions.
9 changes: 8 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"sync"

"github.com/muesli/termenv"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -110,6 +111,7 @@ func init() {
func setGlobalCliOptions() {
// setup global CLI options (available on all CLI commands)
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
rootCmd.PersistentFlags().BoolVar(&persistentOpts.NoColor, "no-color", false, "disable color for table output")

flag := "quiet"
rootCmd.PersistentFlags().BoolP(
Expand Down Expand Up @@ -390,9 +392,14 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
DBStatus: status,
}

noColor := persistentOpts.NoColor
if !noColor && termenv.ColorProfile() == termenv.Ascii {
noColor = true
}

bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter.GetPresenter(presenterConfig, pb),
Value: presenter.GetPresenter(presenterConfig, pb, noColor),
})
}()
return errs
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ require (
github.com/anchore/syft v0.83.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/mitchellh/mapstructure v1.5.0
github.com/muesli/termenv v0.15.1
)

require (
Expand All @@ -76,6 +77,7 @@ require (
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aws/aws-sdk-go v1.44.180 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
Expand Down Expand Up @@ -125,10 +127,11 @@ require (
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand Down
12 changes: 9 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI=
github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
Expand Down Expand Up @@ -622,6 +624,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
Expand All @@ -648,8 +652,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
Expand Down Expand Up @@ -688,6 +692,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
Expand Down Expand Up @@ -1566,4 +1572,4 @@ modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
4 changes: 2 additions & 2 deletions grype/presenter/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ type Presenter interface {

// GetPresenter retrieves a Presenter that matches a CLI option
// TODO dependency cycle with presenter package to sub formats
func GetPresenter(c Config, pb models.PresenterConfig) Presenter {
func GetPresenter(c Config, pb models.PresenterConfig, tableNoColor bool) Presenter {
switch c.format {
case jsonFormat:
return json.NewPresenter(pb)
case tableFormat:
return table.NewPresenter(pb, c.showSuppressed)
return table.NewPresenter(pb, c.showSuppressed, tableNoColor)

// NOTE: cyclonedx is identical to embeddedVEXJSON
// The cyclonedx library only provides two BOM formats: JSON and XML
Expand Down
34 changes: 32 additions & 2 deletions grype/presenter/table/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ type Presenter struct {
packages []pkg.Package
metadataProvider vulnerability.MetadataProvider
showSuppressed bool
noColor bool
}

// NewPresenter is a *Presenter constructor
func NewPresenter(pb models.PresenterConfig, showSuppressed bool) *Presenter {
func NewPresenter(pb models.PresenterConfig, showSuppressed bool, noColor bool) *Presenter {
return &Presenter{
results: pb.Matches,
ignoredMatches: pb.IgnoredMatches,
packages: pb.Packages,
metadataProvider: pb.MetadataProvider,
showSuppressed: showSuppressed,
noColor: noColor,
}
}

Expand Down Expand Up @@ -98,12 +100,40 @@ func (pres *Presenter) Present(output io.Writer) error {
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)

table.AppendBulk(rows)
if !pres.noColor {
for _, row := range rows {
severityColor := getSeverityColor(row[len(row)-1])
table.Rich(row, []tablewriter.Colors{{}, {}, {}, {}, {}, severityColor})
}
} else {
table.AppendBulk(rows)
}

table.Render()

return nil
}

func getSeverityColor(severity string) tablewriter.Colors {
severityFontType, severityColor := tablewriter.Normal, tablewriter.Normal

switch strings.ToLower(severity) {
case "critical":
severityFontType = tablewriter.Bold
severityColor = tablewriter.FgRedColor
case "high":
severityColor = tablewriter.FgRedColor
case "medium":
severityColor = tablewriter.FgYellowColor
case "low":
severityColor = tablewriter.FgGreenColor
case "negligible":
severityColor = tablewriter.FgBlueColor
}

return tablewriter.Colors{severityFontType, severityColor}
}

func removeDuplicateRows(items [][]string) [][]string {
seen := map[string][]string{}
var result [][]string
Expand Down
45 changes: 40 additions & 5 deletions grype/presenter/table/presenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestCreateRow(t *testing.T) {
}
}

func TestTablePresenter(t *testing.T) {
func TestTablePresenter_Color(t *testing.T) {

var buffer bytes.Buffer
matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme)
Expand All @@ -84,7 +84,42 @@ func TestTablePresenter(t *testing.T) {
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, false)
pres := NewPresenter(pb, false, false)

// run presenter
err := pres.Present(&buffer)
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
// validateAgainstDbSchema(t, string(actual))
}

func TestTablePresenter_NoColor(t *testing.T) {

var buffer bytes.Buffer
matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme)

pb := models.PresenterConfig{
Matches: matches,
Packages: packages,
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, false, true)

// run presenter
err := pres.Present(&buffer)
Expand Down Expand Up @@ -121,7 +156,7 @@ func TestEmptyTablePresenter(t *testing.T) {
MetadataProvider: nil,
}

pres := NewPresenter(pb, false)
pres := NewPresenter(pb, false, false)

// run presenter
err := pres.Present(&buffer)
Expand Down Expand Up @@ -183,7 +218,7 @@ func TestHidesIgnoredMatches(t *testing.T) {
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, false)
pres := NewPresenter(pb, false, true)

err := pres.Present(&buffer)
if err != nil {
Expand Down Expand Up @@ -214,7 +249,7 @@ func TestDisplaysIgnoredMatches(t *testing.T) {
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, true)
pres := NewPresenter(pb, true, true)

err := pres.Present(&buffer)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low
package-2 2.2.2 deb CVE-1999-0002 Critical
1 change: 1 addition & 0 deletions internal/config/cli_only_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ package config

type CliOnlyOptions struct {
ConfigPath string
NoColor bool
Verbosity int
}

0 comments on commit 27962c1

Please sign in to comment.