diff --git a/cmd/root.go b/cmd/root.go index c93dba75d60..9ffb3eddfca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/muesli/termenv" "github.com/pkg/profile" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -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( @@ -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 diff --git a/go.mod b/go.mod index 33dcd54e893..ea6fa015cdf 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 @@ -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 diff --git a/go.sum b/go.sum index b236744f47a..c6965d86966 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= \ No newline at end of file +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/grype/presenter/presenter.go b/grype/presenter/presenter.go index 00256d350fd..e6336def368 100644 --- a/grype/presenter/presenter.go +++ b/grype/presenter/presenter.go @@ -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 diff --git a/grype/presenter/table/presenter.go b/grype/presenter/table/presenter.go index e41dd46ec79..842153b8298 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -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, } } @@ -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 diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index cf3682f3ea4..fea1d41468e 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -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) @@ -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) @@ -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) @@ -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 { @@ -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 { diff --git a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden new file mode 100644 index 00000000000..2d492005cf1 --- /dev/null +++ b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden @@ -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 diff --git a/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden b/grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_NoColor.golden similarity index 100% rename from grype/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden rename to grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_NoColor.golden diff --git a/internal/config/cli_only_options.go b/internal/config/cli_only_options.go index 5bb0e49671e..cc2142d9510 100644 --- a/internal/config/cli_only_options.go +++ b/internal/config/cli_only_options.go @@ -2,5 +2,6 @@ package config type CliOnlyOptions struct { ConfigPath string + NoColor bool Verbosity int }