diff --git a/docs/docs/configuration/reporting.md b/docs/docs/configuration/reporting.md
index c6ddc1727a71..389731c6f107 100644
--- a/docs/docs/configuration/reporting.md
+++ b/docs/docs/configuration/reporting.md
@@ -19,10 +19,81 @@ Trivy supports the following formats:
| Secret | ✓ |
| License | ✓ |
+```bash
+$ trivy image -f table golang:1.22.11-alpine3.21
```
-$ trivy image -f table golang:1.12-alpine
+
+
+Result
+
+```
+...
+
+
+Report Summary
+
+┌─────────────────────────────────────────────┬──────────┬─────────────────┬─────────┐
+│ Target │ Type │ Vulnerabilities │ Secrets │
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+│ golang:1.22.11-alpine3.21 (alpine 3.21.2) │ alpine │ 0 │ - │
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+│ Node.js │ node-pkg │ 0 │ - │
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+│ usr/local/go/bin/go │ gobinary │ 0 │ - │
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+│ usr/local/go/bin/gofmt │ gobinary │ 0 │ - │
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+...
+├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
+│ usr/local/go/pkg/tool/linux_amd64/vet │ gobinary │ 0 │ - │
+└─────────────────────────────────────────────┴──────────┴─────────────────┴─────────┘
+
+golang:1.22.11-alpine3.21 (alpine 3.21.2)
+
+Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
+
```
+
+
+#### Summary table
+Before result tables Trivy shows summary table.
+
+
+Report Summary
+
+```
+┌───────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐
+│ Target │ Type │ Vulnerabilities │ Misconfigurations │ Secrets │ Licenses │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ test (alpine 3.20.3) │ alpine │ 2 │ - │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ Java │ jar │ 2 │ - │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ app/Dockerfile │ dockerfile │ - │ 2 │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ requirements.txt │ text │ 0 │ - │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ requirements.txt │ text │ - │ - │ 1 │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ OS Packages │ - │ - │ - │ - │ 1 │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ Java │ - │ - │ - │ - │ 0 │
+└───────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘
+```
+
+
+
+This table:
+
+- include columns for enabled [scanners](../references/terminology.md#scanner) only.
+- Contains separate lines for the same targets but different scanners.
+- `-` means that Trivy didn't scan this target.
+- `0` means that Trivy scanned this target, but found no vulns/misconfigs.
+
+!!! note
+ Use `--no-summary-table` flag to hide summary table.
+
#### Show origins of vulnerable dependencies
| Scanner | Supported |
diff --git a/docs/docs/references/configuration/cli/trivy_config.md b/docs/docs/references/configuration/cli/trivy_config.md
index 7cc65a04e949..fb9d40a8cf69 100644
--- a/docs/docs/references/configuration/cli/trivy_config.md
+++ b/docs/docs/references/configuration/cli/trivy_config.md
@@ -36,6 +36,7 @@ trivy config [flags] DIR
--k8s-version string specify k8s version to validate outdated api by it (example: 1.21.0)
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
+ --no-summary-table hide summary table
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
diff --git a/docs/docs/references/configuration/cli/trivy_convert.md b/docs/docs/references/configuration/cli/trivy_convert.md
index d76d303e3b03..ec01f6014eaf 100644
--- a/docs/docs/references/configuration/cli/trivy_convert.md
+++ b/docs/docs/references/configuration/cli/trivy_convert.md
@@ -27,6 +27,7 @@ trivy convert [flags] RESULT_JSON
--ignore-policy string specify the Rego file path to evaluate each vulnerability
--ignorefile string specify .trivyignore file (default ".trivyignore")
--list-all-pkgs output all packages in the JSON report regardless of vulnerability
+ --no-summary-table hide summary table
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--report string specify a report format for the output (all,summary) (default "all")
diff --git a/docs/docs/references/configuration/cli/trivy_filesystem.md b/docs/docs/references/configuration/cli/trivy_filesystem.md
index dab87fc541fc..409b30357fee 100644
--- a/docs/docs/references/configuration/cli/trivy_filesystem.md
+++ b/docs/docs/references/configuration/cli/trivy_filesystem.md
@@ -64,6 +64,7 @@ trivy filesystem [flags] PATH
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/cli/trivy_image.md b/docs/docs/references/configuration/cli/trivy_image.md
index d9c312602190..8cace7ae9eef 100644
--- a/docs/docs/references/configuration/cli/trivy_image.md
+++ b/docs/docs/references/configuration/cli/trivy_image.md
@@ -82,6 +82,7 @@ trivy image [flags] IMAGE_NAME
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/cli/trivy_kubernetes.md b/docs/docs/references/configuration/cli/trivy_kubernetes.md
index 959532e1c806..27f372ae3b89 100644
--- a/docs/docs/references/configuration/cli/trivy_kubernetes.md
+++ b/docs/docs/references/configuration/cli/trivy_kubernetes.md
@@ -77,6 +77,7 @@ trivy kubernetes [flags] [CONTEXT]
--list-all-pkgs output all packages in the JSON report regardless of vulnerability
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--node-collector-imageref string indicate the image reference for the node-collector scan job (default "ghcr.io/aquasecurity/node-collector:0.3.1")
--node-collector-namespace string specify the namespace in which the node-collector job should be deployed (default "trivy-temp")
--offline-scan do not issue API requests to identify dependencies
diff --git a/docs/docs/references/configuration/cli/trivy_repository.md b/docs/docs/references/configuration/cli/trivy_repository.md
index 38ae6611b595..4df9b3798d56 100644
--- a/docs/docs/references/configuration/cli/trivy_repository.md
+++ b/docs/docs/references/configuration/cli/trivy_repository.md
@@ -63,6 +63,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/cli/trivy_rootfs.md b/docs/docs/references/configuration/cli/trivy_rootfs.md
index 35cc54ff66a3..a02dc2b289e8 100644
--- a/docs/docs/references/configuration/cli/trivy_rootfs.md
+++ b/docs/docs/references/configuration/cli/trivy_rootfs.md
@@ -66,6 +66,7 @@ trivy rootfs [flags] ROOTDIR
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/cli/trivy_sbom.md b/docs/docs/references/configuration/cli/trivy_sbom.md
index 63f855b13335..0ecd234ab457 100644
--- a/docs/docs/references/configuration/cli/trivy_sbom.md
+++ b/docs/docs/references/configuration/cli/trivy_sbom.md
@@ -45,6 +45,7 @@ trivy sbom [flags] SBOM_PATH
--java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1])
--list-all-pkgs output all packages in the JSON report regardless of vulnerability
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/cli/trivy_vm.md b/docs/docs/references/configuration/cli/trivy_vm.md
index 6fe9de30fcd8..3d2349a78ca6 100644
--- a/docs/docs/references/configuration/cli/trivy_vm.md
+++ b/docs/docs/references/configuration/cli/trivy_vm.md
@@ -58,6 +58,7 @@ trivy vm [flags] VM_IMAGE
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
+ --no-summary-table hide summary table
--offline-scan do not issue API requests to identify dependencies
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
diff --git a/docs/docs/references/configuration/config-file.md b/docs/docs/references/configuration/config-file.md
index fe6332522ee0..01cf1a9a27cd 100644
--- a/docs/docs/references/configuration/config-file.md
+++ b/docs/docs/references/configuration/config-file.md
@@ -521,6 +521,9 @@ ignorefile: ".trivyignore"
# Same as '--list-all-pkgs'
list-all-pkgs: false
+# Same as '--no-summary-table'
+no-summary-table: false
+
# Same as '--output'
output: ""
diff --git a/pkg/flag/report_flags.go b/pkg/flag/report_flags.go
index d69443e89547..ee7db0198c77 100644
--- a/pkg/flag/report_flags.go
+++ b/pkg/flag/report_flags.go
@@ -109,6 +109,11 @@ var (
ConfigName: "scan.show-suppressed",
Usage: "[EXPERIMENTAL] show suppressed vulnerabilities",
}
+ NoSummaryTableFlag = Flag[bool]{
+ Name: "no-summary-table",
+ ConfigName: "no-summary-table",
+ Usage: "hide summary table",
+ }
)
// ReportFlagGroup composes common printer flag structs
@@ -128,6 +133,7 @@ type ReportFlagGroup struct {
Severity *Flag[[]string]
Compliance *Flag[string]
ShowSuppressed *Flag[bool]
+ NoSummaryTable *Flag[bool]
}
type ReportOptions struct {
@@ -145,6 +151,7 @@ type ReportOptions struct {
Severities []dbTypes.Severity
Compliance spec.ComplianceSpec
ShowSuppressed bool
+ NoSummaryTable bool
}
func NewReportFlagGroup() *ReportFlagGroup {
@@ -163,6 +170,7 @@ func NewReportFlagGroup() *ReportFlagGroup {
Severity: SeverityFlag.Clone(),
Compliance: ComplianceFlag.Clone(),
ShowSuppressed: ShowSuppressedFlag.Clone(),
+ NoSummaryTable: NoSummaryTableFlag.Clone(),
}
}
@@ -186,6 +194,7 @@ func (f *ReportFlagGroup) Flags() []Flagger {
f.Severity,
f.Compliance,
f.ShowSuppressed,
+ f.NoSummaryTable,
}
}
@@ -198,6 +207,7 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
template := f.Template.Value()
dependencyTree := f.DependencyTree.Value()
listAllPkgs := f.ListAllPkgs.Value()
+ noSummaryTable := f.NoSummaryTable.Value()
if template != "" {
if format == "" {
@@ -227,6 +237,12 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
}
}
+ // "--so-summary" option is available only with "--format table".
+ if noSummaryTable && format != types.FormatTable {
+ noSummaryTable = false
+ log.Warn(`"--no-summary-table" can be used only with "--format table".`)
+ }
+
cs, err := loadComplianceTypes(f.Compliance.Value())
if err != nil {
return ReportOptions{}, xerrors.Errorf("unable to load compliance spec: %w", err)
@@ -259,6 +275,7 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
Severities: toSeverity(f.Severity.Value()),
Compliance: cs,
ShowSuppressed: f.ShowSuppressed.Value(),
+ NoSummaryTable: noSummaryTable,
}, nil
}
diff --git a/pkg/flag/report_flags_test.go b/pkg/flag/report_flags_test.go
index ab4baa53fbff..4cd99fe8aef7 100644
--- a/pkg/flag/report_flags_test.go
+++ b/pkg/flag/report_flags_test.go
@@ -32,6 +32,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
compliance string
debug bool
pkgTypes string
+ noSummaryTable bool
}
tests := []struct {
name string
@@ -115,6 +116,20 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
ListAllPkgs: true,
},
},
+ {
+ name: "invalid option combination: --no-summary-table with --format json",
+ fields: fields{
+ format: "json",
+ noSummaryTable: true,
+ },
+ wantLogs: []string{
+ `"--no-summary-table" can be used only with "--format table".`,
+ },
+ want: flag.ReportOptions{
+ Format: "json",
+ NoSummaryTable: false,
+ },
+ },
{
name: "happy path with output plugin args",
fields: fields{
@@ -184,6 +199,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
setValue(flag.OutputPluginArgFlag.ConfigName, tt.fields.outputPluginArgs)
setValue(flag.SeverityFlag.ConfigName, tt.fields.severities)
setValue(flag.ComplianceFlag.ConfigName, tt.fields.compliance)
+ setValue(flag.NoSummaryTableFlag.ConfigName, tt.fields.noSummaryTable)
// Assert options
f := &flag.ReportFlagGroup{
@@ -199,6 +215,7 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
OutputPluginArg: flag.OutputPluginArgFlag.Clone(),
Severity: flag.SeverityFlag.Clone(),
Compliance: flag.ComplianceFlag.Clone(),
+ NoSummaryTable: flag.NoSummaryTableFlag.Clone(),
}
got, err := f.ToOptions()
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..eeac9794da5f 100644
--- a/pkg/report/table/table.go
+++ b/pkg/report/table/table.go
@@ -10,10 +10,13 @@ import (
"strings"
"github.com/fatih/color"
+ "github.com/samber/lo"
+ "golang.org/x/xerrors"
"github.com/aquasecurity/table"
"github.com/aquasecurity/tml"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
+ "github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)
@@ -29,6 +32,7 @@ var (
// Writer implements Writer and output in tabular form
type Writer struct {
+ Scanners types.Scanners
Severities []dbTypes.Severity
Output io.Writer
@@ -38,6 +42,9 @@ type Writer struct {
// Show suppressed findings
ShowSuppressed bool
+ // Hide summary table
+ NoSummaryTable bool
+
// For misconfigurations
IncludeNonFailures bool
Trace bool
@@ -53,6 +60,15 @@ 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 !tw.NoSummaryTable {
+ 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 +80,69 @@ func (tw Writer) Write(_ context.Context, report types.Report) error {
return nil
}
+func (tw Writer) renderSummary(report types.Report) error {
+ log.WithPrefix("report").Info("Report Summary table contains special symbols",
+ log.String("'-'", "Target didn't scanned"),
+ log.String("'0'", "Target scanned, but didn't contain vulns/misconfigs/secrets/licenses"))
+ // 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)
+ }
+
+ // It should be an impossible case.
+ // But it is possible when Trivy is used as a library.
+ if len(scanners) == 0 {
+ return xerrors.Errorf("unable to find scanners")
+ }
+
+ 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 || result.Class == types.ClassLicenseFile {
+ 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 +176,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/table/table_private_test.go b/pkg/report/table/table_private_test.go
new file mode 100644
index 000000000000..c54c3ea5fb4c
--- /dev/null
+++ b/pkg/report/table/table_private_test.go
@@ -0,0 +1,232 @@
+package table
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/aquasecurity/tml"
+ ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
+ "github.com/aquasecurity/trivy/pkg/types"
+)
+
+var (
+ osVuln = types.Result{
+ Target: "test (alpine 3.20.3)",
+ Class: types.ClassOSPkg,
+ Type: ftypes.Alpine,
+ Vulnerabilities: []types.DetectedVulnerability{
+ {
+ VulnerabilityID: "CVE-2024-9143",
+ PkgName: "libcrypto3",
+ },
+ {
+ VulnerabilityID: "CVE-2024-9143",
+ PkgName: "libssl3",
+ },
+ },
+ }
+ jarVuln = types.Result{
+ Target: "Java",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Jar,
+ Vulnerabilities: []types.DetectedVulnerability{
+ {
+ VulnerabilityID: "CVE-2022-42003",
+ PkgName: "com.fasterxml.jackson.core:jackson-databind",
+ PkgPath: "app/jackson-databind-2.13.4.1.jar",
+ },
+ {
+ VulnerabilityID: "CVE-2021-44832",
+ PkgName: "org.apache.logging.log4j:log4j-core",
+ PkgPath: "app/log4j-core-2.17.0.jar",
+ },
+ },
+ }
+
+ noVuln = types.Result{
+ Target: "requirements.txt",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Pip,
+ }
+
+ dockerfileMisconfig = types.Result{
+ Target: "app/Dockerfile",
+ Class: types.ClassConfig,
+ Type: ftypes.Dockerfile,
+ Misconfigurations: []types.DetectedMisconfiguration{
+ {
+ ID: "DS002",
+ },
+ {
+ ID: "DS026",
+ },
+ },
+ }
+ secret = types.Result{
+ Target: "requirements.txt",
+ Class: types.ClassSecret,
+ Secrets: []types.DetectedSecret{
+ {
+ RuleID: "aws-access-key-id",
+ },
+ },
+ }
+ osLicense = types.Result{
+ Target: "OS Packages",
+ Class: types.ClassLicense,
+ Licenses: []types.DetectedLicense{
+ {
+ Name: "GPL-2.0-only",
+ },
+ },
+ }
+
+ jarLicense = types.Result{
+ Target: "Java",
+ Class: types.ClassLicense,
+ }
+ fileLicense = types.Result{
+ Target: "Loose File License(s)",
+ Class: types.ClassLicenseFile,
+ }
+)
+
+func Test_renderSummary(t *testing.T) {
+ tests := []struct {
+ name string
+ scanners types.Scanners
+ noSummaryTable bool
+ report types.Report
+ want string
+ }{
+ {
+ name: "happy path all scanners",
+ scanners: []types.Scanner{
+ types.VulnerabilityScanner,
+ types.MisconfigScanner,
+ types.SecretScanner,
+ types.LicenseScanner,
+ },
+ report: types.Report{
+ Results: []types.Result{
+ osVuln,
+ jarVuln,
+ dockerfileMisconfig,
+ secret,
+ osLicense,
+ jarLicense,
+ fileLicense,
+ },
+ },
+ want: `
+Report Summary
+
+┌───────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐
+│ Target │ Type │ Vulnerabilities │ Misconfigurations │ Secrets │ Licenses │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ test (alpine 3.20.3) │ alpine │ 2 │ - │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ Java │ jar │ 2 │ - │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ app/Dockerfile │ dockerfile │ - │ 2 │ - │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ requirements.txt │ text │ - │ - │ 1 │ - │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ OS Packages │ - │ - │ - │ - │ 1 │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ Java │ - │ - │ - │ - │ 0 │
+├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
+│ Loose File License(s) │ - │ - │ - │ - │ - │
+└───────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘
+`,
+ },
+ {
+ name: "happy path vuln scanner only",
+ scanners: []types.Scanner{
+ types.VulnerabilityScanner,
+ },
+ report: types.Report{
+ Results: []types.Result{
+ osVuln,
+ jarVuln,
+ },
+ },
+ want: `
+Report Summary
+
+┌──────────────────────┬────────┬─────────────────┐
+│ Target │ Type │ Vulnerabilities │
+├──────────────────────┼────────┼─────────────────┤
+│ test (alpine 3.20.3) │ alpine │ 2 │
+├──────────────────────┼────────┼─────────────────┤
+│ Java │ jar │ 2 │
+└──────────────────────┴────────┴─────────────────┘
+`,
+ },
+ {
+ name: "happy path no vulns + secret",
+ scanners: []types.Scanner{
+ types.VulnerabilityScanner,
+ types.SecretScanner,
+ },
+ report: types.Report{
+ Results: []types.Result{
+ noVuln,
+ secret,
+ },
+ },
+ want: `
+Report Summary
+
+┌──────────────────┬──────┬─────────────────┬─────────┐
+│ Target │ Type │ Vulnerabilities │ Secrets │
+├──────────────────┼──────┼─────────────────┼─────────┤
+│ requirements.txt │ pip │ 0 │ - │
+├──────────────────┼──────┼─────────────────┼─────────┤
+│ requirements.txt │ text │ - │ 1 │
+└──────────────────┴──────┴─────────────────┴─────────┘
+`,
+ },
+ {
+ name: "happy path vuln scanner only",
+ scanners: []types.Scanner{
+ types.VulnerabilityScanner,
+ },
+ report: types.Report{
+ Results: []types.Result{
+ osVuln,
+ jarVuln,
+ },
+ },
+ want: `
+Report Summary
+
+┌──────────────────────┬────────┬─────────────────┐
+│ Target │ Type │ Vulnerabilities │
+├──────────────────────┼────────┼─────────────────┤
+│ test (alpine 3.20.3) │ alpine │ 2 │
+├──────────────────────┼────────┼─────────────────┤
+│ Java │ jar │ 2 │
+└──────────────────────┴────────┴─────────────────┘
+`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tml.DisableFormatting()
+ tableWritten := bytes.Buffer{}
+ writer := Writer{
+ Output: &tableWritten,
+ Scanners: tt.scanners,
+ NoSummaryTable: tt.noSummaryTable,
+ }
+ err := writer.renderSummary(tt.report)
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, tableWritten.String())
+ })
+ }
+}
diff --git a/pkg/report/table/table_test.go b/pkg/report/table/table_test.go
index d52dda0dc232..d88e8ee355c0 100644
--- a/pkg/report/table/table_test.go
+++ b/pkg/report/table/table_test.go
@@ -16,15 +16,22 @@ import (
func TestWriter_Write(t *testing.T) {
testCases := []struct {
name string
+ scanners types.Scanners
+ noSummaryTable bool
results types.Results
- expectedOutput string
+ wantOutput string
+ wantError string
includeNonFailures bool
}{
{
name: "vulnerability and custom resource",
+ scanners: types.Scanners{
+ types.VulnerabilityScanner,
+ },
results: types.Results{
{
Target: "test",
+ Type: ftypes.Jar,
Class: types.ClassLangPkg,
Vulnerabilities: []types.DetectedVulnerability{
{
@@ -48,9 +55,17 @@ func TestWriter_Write(t *testing.T) {
},
},
},
- expectedOutput: `
-test ()
-=======
+ wantOutput: `
+Report Summary
+
+┌────────┬──────┬─────────────────┐
+│ Target │ Type │ Vulnerabilities │
+├────────┼──────┼─────────────────┤
+│ test │ jar │ 1 │
+└────────┴──────┴─────────────────┘
+
+test (jar)
+==========
Total: 1 (MEDIUM: 0, HIGH: 1)
┌─────────┬───────────────┬──────────┬──────────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐
@@ -63,13 +78,51 @@ Total: 1 (MEDIUM: 0, HIGH: 1)
},
{
name: "no vulns",
+ scanners: types.Scanners{
+ types.VulnerabilityScanner,
+ },
results: types.Results{
{
Target: "test",
Class: types.ClassLangPkg,
+ Type: ftypes.Jar,
},
},
- expectedOutput: ``,
+ wantOutput: `
+Report Summary
+
+┌────────┬──────┬─────────────────┐
+│ Target │ Type │ Vulnerabilities │
+├────────┼──────┼─────────────────┤
+│ test │ jar │ 0 │
+└────────┴──────┴─────────────────┘
+`,
+ },
+ {
+ name: "no summary",
+ scanners: types.Scanners{
+ types.VulnerabilityScanner,
+ },
+ noSummaryTable: true,
+ results: types.Results{
+ {
+ Target: "test",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Jar,
+ },
+ },
+ wantOutput: ``,
+ },
+ {
+ name: "no scanners",
+ results: types.Results{
+ {
+ Target: "test",
+ Class: types.ClassLangPkg,
+ Type: ftypes.Jar,
+ },
+ },
+ wantError: "unable to find scanners",
},
}
@@ -85,10 +138,17 @@ Total: 1 (MEDIUM: 0, HIGH: 1)
dbTypes.SeverityHigh,
dbTypes.SeverityMedium,
},
+ Scanners: tc.scanners,
+ NoSummaryTable: tc.noSummaryTable,
}
err := writer.Write(nil, types.Report{Results: tc.results})
+ if tc.wantError != "" {
+ require.Error(t, err)
+ return
+ }
+
require.NoError(t, err)
- assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name)
+ assert.Equal(t, tc.wantOutput, tableWritten.String(), tc.name)
})
}
}
diff --git a/pkg/report/writer.go b/pkg/report/writer.go
index f25d579d66ef..81d76dca69c7 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,
@@ -53,6 +54,7 @@ func Write(ctx context.Context, report types.Report, option flag.Options) (err e
Trace: option.Trace,
LicenseRiskThreshold: option.LicenseRiskThreshold,
IgnoredLicenses: option.IgnoredLicenses,
+ NoSummaryTable: option.NoSummaryTable,
}
case types.FormatJSON:
writer = &JSONWriter{