From b29efc812c0112ca1860e4cf12b983de024fc8c1 Mon Sep 17 00:00:00 2001 From: Shane Dell Date: Mon, 8 May 2023 18:12:10 -0400 Subject: [PATCH] Colorize severity in table output - 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. Closes #225 Signed-off-by: Shane Dell --- cmd/root.go | 3 +- grype/presenter/presenter.go | 6 +-- grype/presenter/table/presenter.go | 34 ++++++++++++++- grype/presenter/table/presenter_test.go | 41 +++++++++++++++++-- .../snapshot/TestTablePresenter_Color.golden | 3 ++ ...lden => TestTablePresenter_NoColor.golden} | 0 internal/config/cli_only_options.go | 1 + 7 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 grype/presenter/table/test-fixtures/snapshot/TestTablePresenter_Color.golden rename grype/presenter/table/test-fixtures/snapshot/{TestTablePresenter.golden => TestTablePresenter_NoColor.golden} (100%) diff --git a/cmd/root.go b/cmd/root.go index 9d39eb3cf553..ac5254ac3c36 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -110,6 +110,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( @@ -392,7 +393,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha bus.Publish(partybus.Event{ Type: event.VulnerabilityScanningFinished, - Value: presenter.GetPresenter(presenterConfig, pb), + Value: presenter.GetPresenter(presenterConfig, pb, persistentOpts.NoColor), }) }() return errs diff --git a/grype/presenter/presenter.go b/grype/presenter/presenter.go index 3ca03e42cc26..bdea13398c38 100644 --- a/grype/presenter/presenter.go +++ b/grype/presenter/presenter.go @@ -19,15 +19,15 @@ 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: if c.showSuppressed { - return table.NewPresenter(pb) + return table.NewPresenter(pb, tableNoColor) } - return table.NewPresenter(pb) + return table.NewPresenter(pb, 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 d48e9bf80d8e..53223345c284 100644 --- a/grype/presenter/table/presenter.go +++ b/grype/presenter/table/presenter.go @@ -25,15 +25,17 @@ type Presenter struct { ignoredMatches []match.IgnoredMatch packages []pkg.Package metadataProvider vulnerability.MetadataProvider + noColor bool } // NewPresenter is a *Presenter constructor -func NewPresenter(pb models.PresenterConfig) *Presenter { +func NewPresenter(pb models.PresenterConfig, noColor bool) *Presenter { return &Presenter{ results: pb.Matches, ignoredMatches: pb.IgnoredMatches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, + noColor: noColor, } } @@ -94,12 +96,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 bd9dd1a9a2c6..935aabf3382f 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) + pres := NewPresenter(pb, 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, true) // run presenter err := pres.Present(&buffer) @@ -121,7 +156,7 @@ func TestEmptyTablePresenter(t *testing.T) { MetadataProvider: nil, } - pres := NewPresenter(pb) + pres := NewPresenter(pb, true) // run presenter err := pres.Present(&buffer) 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 000000000000..2d492005cf18 --- /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 5bb0e49671e7..cc2142d9510d 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 }