diff --git a/pkg/report/table/summary.go b/pkg/report/table/summary.go new file mode 100644 index 000000000000..e9bffc242050 --- /dev/null +++ b/pkg/report/table/summary.go @@ -0,0 +1,86 @@ +package table + +import ( + "github.com/aquasecurity/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Scanner interface { + Header() string + Alignment() table.Alignment + + // Count returns the number of findings, but -1 if the scanner is not applicable + Count(result types.Result) int +} + +func NewScanner(scanner types.Scanner) Scanner { + switch scanner { + case types.VulnerabilityScanner: + return VulnerabilityScanner{} + case types.MisconfigScanner: + return MisconfigScanner{} + case types.SecretScanner: + return SecretScanner{} + case types.LicenseScanner: + return LicenseScanner{} + } + return nil +} + +type scannerAlignment struct{} + +func (s scannerAlignment) Alignment() table.Alignment { + return table.AlignCenter +} + +type VulnerabilityScanner struct{ scannerAlignment } + +func (s VulnerabilityScanner) Header() string { + return "Vulnerabilities" +} + +func (s VulnerabilityScanner) Count(result types.Result) int { + if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg { + return len(result.Vulnerabilities) + } + return -1 +} + +type MisconfigScanner struct{ scannerAlignment } + +func (s MisconfigScanner) Header() string { + return "Misconfigurations" +} + +func (s MisconfigScanner) Count(result types.Result) int { + if result.Class == types.ClassConfig { + return len(result.Misconfigurations) + } + return -1 +} + +type SecretScanner struct{ scannerAlignment } + +func (s SecretScanner) Header() string { + return "Secrets" +} + +func (s SecretScanner) Count(result types.Result) int { + if result.Class == types.ClassSecret { + return len(result.Secrets) + } + return -1 +} + +type LicenseScanner struct{ scannerAlignment } + +func (s LicenseScanner) Header() string { + return "Licenses" +} + +func (s LicenseScanner) Count(result types.Result) int { + if result.Class == types.ClassLicense { + return len(result.Licenses) + } + return -1 +} diff --git a/pkg/report/table/table.go b/pkg/report/table/table.go index 8bfa75922013..08f7651b5173 100644 --- a/pkg/report/table/table.go +++ b/pkg/report/table/table.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/fatih/color" + "github.com/samber/lo" + "golang.org/x/xerrors" "github.com/aquasecurity/table" "github.com/aquasecurity/tml" @@ -29,6 +31,7 @@ var ( // Writer implements Writer and output in tabular form type Writer struct { + Scanners types.Scanners Severities []dbTypes.Severity Output io.Writer @@ -53,6 +56,13 @@ type Renderer interface { // Write writes the result on standard output func (tw Writer) Write(_ context.Context, report types.Report) error { + if !tw.isOutputToTerminal() { + tml.DisableFormatting() + } + + if err := tw.renderSummary(report); err != nil { + return xerrors.Errorf("failed to render summary: %w", err) + } for _, result := range report.Results { // Not display a table of custom resources @@ -64,6 +74,60 @@ func (tw Writer) Write(_ context.Context, report types.Report) error { return nil } +func (tw Writer) renderSummary(report types.Report) error { + // Fprintln has a bug + if err := tml.Fprintf(tw.Output, "\nReport Summary\n\n"); err != nil { + return err + } + + t := newTableWriter(tw.Output, tw.isOutputToTerminal()) + t.SetAutoMerge(false) + t.SetColumnMaxWidth(80) + + var scanners []Scanner + for _, scanner := range tw.Scanners { + s := NewScanner(scanner) + if lo.IsNil(s) { + continue + } + scanners = append(scanners, s) + } + + headers := []string{ + "Target", + "Type", + } + alignments := []table.Alignment{ + table.AlignLeft, + table.AlignCenter, + } + for _, scanner := range scanners { + headers = append(headers, scanner.Header()) + alignments = append(alignments, scanner.Alignment()) + } + t.SetHeaders(headers...) + t.SetAlignment(alignments...) + + for _, result := range report.Results { + resultType := string(result.Type) + if result.Class == types.ClassSecret { + resultType = "text" + } else if result.Class == types.ClassLicense { + resultType = "-" + } + rows := []string{ + result.Target, + resultType, + } + for _, scanner := range scanners { + rows = append(rows, tw.colorizeCount(scanner.Count(result))) + } + t.AddRows(rows) + } + t.Render() + return nil +} + func (tw Writer) write(result types.Result) { if result.IsEmpty() && result.Class != types.ClassOSPkg { return @@ -97,6 +161,17 @@ func (tw Writer) isOutputToTerminal() bool { return IsOutputToTerminal(tw.Output) } +func (tw Writer) colorizeCount(count int) string { + if count < 0 { + return "-" + } + sprintf := fmt.Sprintf + if count != 0 && tw.isOutputToTerminal() { + sprintf = color.New(color.FgHiRed).SprintfFunc() + } + return sprintf("%d", count) +} + func newTableWriter(output io.Writer, isTerminal bool) *table.Table { tableWriter := table.New(output) if isTerminal { // use ansi output if we're not piping elsewhere diff --git a/pkg/report/writer.go b/pkg/report/writer.go index f25d579d66ef..c8079fa0f969 100644 --- a/pkg/report/writer.go +++ b/pkg/report/writer.go @@ -45,6 +45,7 @@ func Write(ctx context.Context, report types.Report, option flag.Options) (err e switch option.Format { case types.FormatTable: writer = &table.Writer{ + Scanners: option.Scanners, Output: output, Severities: option.Severities, Tree: option.DependencyTree,