/
githubmirror
/
trivy
Обзор
Документация
Войти
/
githubmirror
/
trivy
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
pkg/report/table/summary.go
317 строк
8 KB
DmitriyLewen
fix(report): don't panic when report contains vulns, but doesn't contain packages for `table` format (#8549)
10 июн 2025, 08:22
Не верифицирован
10 июн 2025, 08:22
87fda76
Код
Авторство
О чём код?
package table import ( "bytes" "fmt" "slices" "sort" "github.com/fatih/color" "github.com/samber/lo" "github.com/aquasecurity/table" "github.com/aquasecurity/tml" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/scan/langpkg" "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 String() string // Required to show correct logs } 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 } func (s VulnerabilityScanner) String() string { return string(types.VulnerabilityScanner) } 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 } func (s MisconfigScanner) String() string { return string(types.MisconfigScanner) } 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 } func (s SecretScanner) String() string { return string(types.SecretScanner) } type LicenseScanner struct{ scannerAlignment } func (s LicenseScanner) Header() string { return "Licenses" } func (s LicenseScanner) Count(result types.Result) int { if result.Class == types.ClassLicense || result.Class == types.ClassLicenseFile { return len(result.Licenses) } return -1 } func (s LicenseScanner) String() string { return string(types.LicenseScanner) } type summaryRenderer struct { w *bytes.Buffer isTerminal bool scanners []Scanner logger *log.Logger } func NewSummaryRenderer(buf *bytes.Buffer, isTerminal bool, scanners types.Scanners) *summaryRenderer { if !isTerminal { tml.DisableFormatting() } var ss []Scanner for _, scanner := range scanners { s := NewScanner(scanner) if lo.IsNil(s) { continue } ss = append(ss, s) } return &summaryRenderer{ w: buf, isTerminal: isTerminal, scanners: ss, logger: log.WithPrefix("report"), } } func (r *summaryRenderer) Render(report types.Report) { if len(r.scanners) == 0 { r.logger.Warn("No enabled scanners found. Summary table will not be displayed.") return } r.printf("\n<underline><bold>Report Summary</bold></underline>\n\n") t := newTableWriter(r.w, r.isTerminal) t.SetAutoMerge(false) t.SetColumnMaxWidth(80) headers := []string{ "Target", "Type", } alignments := []table.Alignment{ table.AlignLeft, table.AlignCenter, } for _, scanner := range r.scanners { headers = append(headers, scanner.Header()) alignments = append(alignments, scanner.Alignment()) } t.SetHeaders(headers...) t.SetAlignment(alignments...) for _, result := range r.splitAggregatedPackages(report.Results) { resultType := string(result.Type) switch result.Class { case types.ClassSecret: resultType = "text" case types.ClassLicense, types.ClassLicenseFile: resultType = "-" } rows := []string{ result.Target, resultType, } for _, scanner := range r.scanners { rows = append(rows, r.colorizeCount(scanner.Count(result))) } t.AddRows(rows) } if len(report.Results) == 0 { r.showEmptyResultsWarning() alignments[0] = table.AlignCenter t.SetAlignment(alignments...) t.AddRows(slices.Repeat([]string{"-"}, len(r.scanners)+2)) } t.Render() // Show legend r.printf("Legend:\n" + "- '-': Not scanned\n" + "- '0': Clean (no security findings detected)\n\n") } func (r *summaryRenderer) printf(format string, args ...any) { // nolint _ = tml.Fprintf(r.w, format, args...) } // showEmptyResultsWarning shows WARN why the results array is empty based on the enabled scanners. // We need to separate the vuln/misconfig and secret/license scanners, // because the results array contains results without findings for vulns/misconfig only. func (r *summaryRenderer) showEmptyResultsWarning() { resultByFiles := []Scanner{ NewScanner(types.VulnerabilityScanner), NewScanner(types.MisconfigScanner), } resultByFindings := []Scanner{ NewScanner(types.SecretScanner), NewScanner(types.LicenseScanner), } if scanners := lo.Intersect(resultByFiles, r.scanners); len(scanners) > 0 { r.logger.Warn("Supported files for scanner(s) not found.", log.Any("scanners", scanners)) } if scanners := lo.Intersect(resultByFindings, r.scanners); len(scanners) > 0 { r.logger.Info("No issues detected with scanner(s).", log.Any("scanners", scanners)) } } // splitAggregatedPackages splits aggregated packages into different results with path as target. // Other results will be returned as is. func (r *summaryRenderer) 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, r.splitAggregatedVulns(result)...) newResults = append(newResults, splitAggregatedLicenses(result)...) } return newResults } func (r *summaryRenderer) splitAggregatedVulns(result types.Result) types.Results { // Handle case when result doesn't contain Package // cf. https://github.com/aquasecurity/trivy/discussions/8537 if len(result.Packages) == 0 && len(result.Vulnerabilities) > 0 { r.logger.Warn("Packages not found unexpectedly", log.String("target", result.Target)) return types.Results{ result, } } // Save packages to display them in the table even if no vulnerabilities were found resultMap := lo.SliceToMap(result.Packages, func(pkg ftypes.Package) (string, *types.Result) { filePath := rootJarFromPath(pkg.FilePath) return filePath, &types.Result{ Target: lo.Ternary(filePath != "", filePath, result.Target), Class: result.Class, Type: result.Type, } }) for _, vuln := range result.Vulnerabilities { pkgPath := rootJarFromPath(vuln.PkgPath) if res, ok := resultMap[pkgPath]; !ok { r.logger.Warn("Package not found unexpectedly", log.String("package_path", pkgPath), log.String("vuln_id", vuln.VulnerabilityID)) } else { res.Vulnerabilities = append(res.Vulnerabilities, vuln) } } newResults := lo.Values(resultMap) sort.Slice(newResults, func(i, j int) bool { return newResults[i].Target < newResults[j].Target }) return lo.FromSlicePtr(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 (r *summaryRenderer) colorizeCount(count int) string { if count < 0 { return "-" } sprintf := fmt.Sprintf if count != 0 && r.isTerminal { sprintf = color.New(color.FgHiRed).SprintfFunc() } return sprintf("%d", count) }