Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add report summary table #8177

Merged
merged 49 commits into from
Mar 4, 2025
Merged
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ff62336
feat(cli): add report summary
knqyf263 Dec 25, 2024
ca75fc9
test: fix table tests
DmitriyLewen Jan 16, 2025
ef26940
feat: add `--no-summary` flag
DmitriyLewen Jan 16, 2025
3ea064f
test: add test for `renderSummary`
DmitriyLewen Jan 16, 2025
8b2acf7
mage docs:generate
DmitriyLewen Jan 16, 2025
751823a
refactor: rename `no-summary` to `no-summary-table`
DmitriyLewen Jan 17, 2025
37a072d
fix: linter errors
DmitriyLewen Jan 17, 2025
f4f46a5
test: refactor secret result
DmitriyLewen Jan 17, 2025
4292e65
Merge branch 'main' of github.com:aquasecurity/trivy into feat/summar…
DmitriyLewen Jan 17, 2025
19fc0fe
docs: add info about summary table
DmitriyLewen Jan 17, 2025
a27f879
feat: add logs about `-` and `0` in summary table
DmitriyLewen Jan 17, 2025
239251b
fix: linter error
DmitriyLewen Jan 17, 2025
8b9d908
Merge branch 'main' of github.com:aquasecurity/trivy into feat/summar…
DmitriyLewen Jan 29, 2025
e11221a
refactor: use footer instead of log for legend
DmitriyLewen Jan 29, 2025
5f61893
refactor: use log when results array is empty
DmitriyLewen Jan 29, 2025
85edf16
chore: add comment for showEmptyResultsWarning
DmitriyLewen Jan 29, 2025
d5ca966
refactor
DmitriyLewen Jan 29, 2025
eb4d2fa
refactor: hide empty results for OS packages license and file licenses
DmitriyLewen Jan 29, 2025
a57dcc6
fix: add LicenseFiles into summary table
DmitriyLewen Jan 29, 2025
4f349c5
test: add LicenseFiles in test
DmitriyLewen Jan 29, 2025
84169b4
feat: split aggregated pkgs
DmitriyLewen Jan 29, 2025
892d6de
fix: show packages without vulns
DmitriyLewen Jan 29, 2025
db0cbe1
fix: tests
DmitriyLewen Jan 29, 2025
28ebb24
refactor: don't show empty vuln table title for OS pkgs
DmitriyLewen Jan 29, 2025
0166a45
refactor: use info log when results didn't find
DmitriyLewen Jan 29, 2025
5f13964
fix: typo
DmitriyLewen Jan 29, 2025
7f863b9
refactor: show legend after table
DmitriyLewen Jan 29, 2025
2628b70
docs: fix typo
DmitriyLewen Feb 4, 2025
c78bfe5
fix: nested list
DmitriyLewen Feb 4, 2025
ba8f756
refactor: stop iter vulns
DmitriyLewen Feb 4, 2025
d37ad35
refactor: split license functions
DmitriyLewen Feb 4, 2025
786e704
refactor: use error for no-summary + no-table format
DmitriyLewen Feb 4, 2025
9c3e8ce
refactor: add empty table with `-`
DmitriyLewen Feb 4, 2025
878b9f4
refactor: use `Renderer` interface for summary table
DmitriyLewen Feb 5, 2025
4b6214b
fix: linter error
DmitriyLewen Feb 5, 2025
29388c4
Merge branch 'main' into 'feat/summary_table'
DmitriyLewen Mar 3, 2025
8de06d7
refactor: after rebase
DmitriyLewen Mar 3, 2025
5ea3e4f
fix(report): skip applications without licenses
DmitriyLewen Mar 3, 2025
1159d63
refactor: enable `--scanners` for convert mode
DmitriyLewen Mar 3, 2025
5b048c1
fix: linter errors
DmitriyLewen Mar 3, 2025
fd00dc9
mage docs:generate
DmitriyLewen Mar 3, 2025
67a729c
refactor: add logger into summaryRenderer
DmitriyLewen Mar 3, 2025
f648dc6
Merge branch 'main' into 'feat/summary_table'
DmitriyLewen Mar 4, 2025
ea453d8
refactor: change --no-summary-table to -tables-mode
DmitriyLewen Mar 4, 2025
9d58ab2
docs: remove not for `--table-mode` flag
DmitriyLewen Mar 4, 2025
5b6e9ea
docs: fix typo
DmitriyLewen Mar 4, 2025
71a53b2
docs: fix typo
DmitriyLewen Mar 4, 2025
df6c0de
refactor: use Info log for secrets/licenses
DmitriyLewen Mar 4, 2025
014e4c4
refactor: use AlignCenter for empty summary table
DmitriyLewen Mar 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: split aggregated pkgs
DmitriyLewen committed Jan 29, 2025
commit 84169b4fc9922ebb285938dc0403b16ef5680764
68 changes: 67 additions & 1 deletion pkg/report/table/table.go
Original file line number Diff line number Diff line change
@@ -7,15 +7,18 @@ import (
"os"
"runtime"
"slices"
"sort"
"strings"

"github.com/aquasecurity/trivy/pkg/scanner/langpkg"
"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"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
xstrings "github.com/aquasecurity/trivy/pkg/x/strings"
@@ -129,7 +132,7 @@ func (tw Writer) renderSummary(report types.Report) error {
t.SetHeaders(headers...)
t.SetAlignment(alignments...)

for _, result := range report.Results {
for _, result := range splitAggregatedPackages(report.Results) {
resultType := string(result.Type)
if result.Class == types.ClassSecret {
resultType = "text"
@@ -179,6 +182,69 @@ func (tw Writer) showEmptyResultsWarning() {
log.WithPrefix("report").Warn(strings.Join(warnStrings, " "))
}

// splitAggregatedPackages splits aggregated packages into different results with path as target.
// Other results will be returned as is.
func splitAggregatedPackages(results types.Results) types.Results {
var newResults types.Results

for _, result := range results {
if !slices.Contains(ftypes.AggregatingTypes, result.Type) &&
// License results from applications don't have `Type`.
(result.Class != types.ClassLicense || !slices.Contains(lo.Values(langpkg.PkgTargets), result.Target)) {
newResults = append(newResults, result)
continue
}

newResults = append(newResults, splitAggregatedVulns(result)...)
newResults = append(newResults, splitAggregatedLicenses(result)...)

}
return newResults
}

func splitAggregatedVulns(result types.Result) types.Results {
var newResults types.Results

vulns := make(map[string][]types.DetectedVulnerability)
for _, vuln := range result.Vulnerabilities {
pkgPath := rootJarFromPath(vuln.PkgPath)
vulns[pkgPath] = append(vulns[pkgPath], vuln)
}
for pkgPath, v := range vulns {
newResult := result
newResult.Target = lo.Ternary(pkgPath != "", pkgPath, result.Target)
newResult.Vulnerabilities = v

newResults = append(newResults, newResult)
}

sort.Slice(newResults, func(i, j int) bool {
return newResults[i].Target < newResults[j].Target
})
return newResults
}

func splitAggregatedLicenses(result types.Result) types.Results {
var newResults types.Results

licenses := make(map[string][]types.DetectedLicense)
for _, license := range result.Licenses {
licenses[license.FilePath] = append(licenses[license.FilePath], license)
}
for filePath, l := range licenses {
newResult := result
newResult.Target = lo.Ternary(filePath != "", filePath, result.Target)
newResult.Licenses = l

newResults = append(newResults, newResult)
}

sort.Slice(newResults, func(i, j int) bool {
return newResults[i].Target < newResults[j].Target
})
return newResults
}

func (tw Writer) write(result types.Result) {
if result.IsEmpty() && result.Class != types.ClassOSPkg {
return
110 changes: 82 additions & 28 deletions pkg/report/table/table_private_test.go
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ var (
},
},
}
jarVuln = types.Result{
jarVulns = types.Result{
Target: "Java",
Class: types.ClassLangPkg,
Type: ftypes.Jar,
@@ -41,7 +41,35 @@ var (
{
VulnerabilityID: "CVE-2021-44832",
PkgName: "org.apache.logging.log4j:log4j-core",
PkgPath: "app/log4j-core-2.17.0.jar",
PkgPath: "app/jackson-databind-2.13.4.1.jar/nested/app2/log4j-core-2.17.0.jar",
},
},
}

npmVulns = types.Result{
Target: "Node.js",
Class: types.ClassLangPkg,
Type: ftypes.NodePkg,
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2022-37601",
PkgName: "loader-utils",
PkgPath: "loader-utils/package.json",
},
{
VulnerabilityID: "CVE-2022-37599",
PkgName: "loader-utils",
PkgPath: "loader-utils/package.json",
},
{
VulnerabilityID: "CVE-2021-23566",
PkgName: "nanoid",
PkgPath: "nanoid/package.json",
},
{
VulnerabilityID: "CVE-2024-55565",
PkgName: "nanoid",
PkgPath: "nanoid/package.json",
},
},
}
@@ -88,6 +116,24 @@ var (
Target: "Java",
Class: types.ClassLicense,
}

npmLicenses = types.Result{
Target: "Node.js",
Class: types.ClassLicense,
Licenses: []types.DetectedLicense{
{
Name: "MIT",
FilePath: "loader-utils/package.json",
Category: "notice",
},
{
Name: "MIT",
FilePath: "nanoid/package.json",
Category: "notice",
},
},
}

fileLicense = types.Result{
Target: "Loose File License(s)",
Class: types.ClassLicenseFile,
@@ -118,11 +164,13 @@ func Test_renderSummary(t *testing.T) {
report: types.Report{
Results: []types.Result{
osVuln,
jarVuln,
jarVulns,
npmVulns,
dockerfileMisconfig,
secret,
osLicense,
jarLicense,
npmLicenses,
fileLicense,
},
},
@@ -132,23 +180,29 @@ Report Summary
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
┌───────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐
│ 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) │ - │ - │ - │ - │ 1 │
└───────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘
┌───────────────────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐
│ Target │ Type │ Vulnerabilities │ Misconfigurations │ Secrets │ Licenses │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ test (alpine 3.20.3) │ alpine │ 2 │ - │ - │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ app/jackson-databind-2.13.4.1.jar │ jar │ 2 │ - │ - │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ loader-utils/package.json │ node-pkg │ 2 │ - │ - │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ nanoid/package.json │ node-pkg │ 2 │ - │ - │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ app/Dockerfile │ dockerfile │ - │ 2 │ - │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ requirements.txt │ text │ - │ - │ 1 │ - │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ OS Packages │ - │ - │ - │ - │ 1 │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ loader-utils/package.json │ - │ - │ - │ - │ 1 │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ nanoid/package.json │ - │ - │ - │ - │ 1 │
├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤
│ Loose File License(s) │ - │ - │ - │ - │ 1 │
└───────────────────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘
`,
},
{
@@ -159,7 +213,7 @@ Legend:
report: types.Report{
Results: []types.Result{
osVuln,
jarVuln,
jarVulns,
},
},
want: `
@@ -168,13 +222,13 @@ Report Summary
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
┌──────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├──────────────────────┼────────┼─────────────────┤
│ test (alpine 3.20.3) │ alpine │ 2 │
├──────────────────────┼────────┼─────────────────┤
Java │ jar │ 2 │
└──────────────────────┴────────┴─────────────────┘
┌───────────────────────────────────┬────────┬─────────────────┐
Target │ Type │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ test (alpine 3.20.3) │ alpine │ 2 │
├───────────────────────────────────┼────────┼─────────────────┤
app/jackson-databind-2.13.4.1.jar │ jar │ 2 │
└───────────────────────────────────┴────────┴─────────────────┘
`,
},
{
6 changes: 6 additions & 0 deletions pkg/report/table/table_test.go
Original file line number Diff line number Diff line change
@@ -58,6 +58,9 @@ func TestWriter_Write(t *testing.T) {
wantOutput: `
Report Summary

Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
┌────────┬──────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────┼──────┼─────────────────┤
@@ -91,6 +94,9 @@ Total: 1 (MEDIUM: 0, HIGH: 1)
wantOutput: `
Report Summary

Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
┌────────┬──────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────┼──────┼─────────────────┤
4 changes: 2 additions & 2 deletions pkg/report/table/vulnerability.go
Original file line number Diff line number Diff line change
@@ -139,8 +139,6 @@ func (r *vulnerabilityRenderer) setVulnerabilityRows(tw *table.Table, vulns []ty
for _, v := range vulns {
lib := v.PkgName
if v.PkgPath != "" {
// get path to root jar
// for other languages return unchanged path
pkgPath := rootJarFromPath(v.PkgPath)
fileName := filepath.Base(pkgPath)
lib = fmt.Sprintf("%s (%s)", v.PkgName, fileName)
@@ -373,6 +371,8 @@ var jarExtensions = []string{
".ear",
}

// rootJarFromPath returns path to root jar.
// For other languages return unchanged path
func rootJarFromPath(path string) string {
// File paths are always forward-slashed in Trivy
paths := strings.Split(path, "/")