diff --git a/PROJECTS/simple-vulnerability-scanner/internal/cli/output.go b/PROJECTS/simple-vulnerability-scanner/internal/cli/output.go index 79aa1f7e..f4d1d5e2 100644 --- a/PROJECTS/simple-vulnerability-scanner/internal/cli/output.go +++ b/PROJECTS/simple-vulnerability-scanner/internal/cli/output.go @@ -5,6 +5,7 @@ package cli import ( "fmt" + "sort" "strings" "github.com/CarterPerez-dev/angela/internal/pypi" @@ -92,7 +93,7 @@ func PrintSkipped(updates []types.UpdateResult) { fmt.Println() } -// PrintVulnerabilities displays a formatted list of security advisories +// PrintVulnerabilities displays security advisories in compact or verbose mode func PrintVulnerabilities( vulns map[string][]types.Vulnerability, ) { @@ -100,36 +101,212 @@ func PrintVulnerabilities( return } - fmt.Printf(" %s\n", redBold("Vulnerabilities found:")) + total := 0 + for _, vl := range vulns { + total += len(vl) + } - for pkg, vlist := range vulns { - fmt.Printf(" %s\n", white(pkg)) - for _, v := range vlist { - sevColor := severityColorFn(v.Severity) + fmt.Printf( + " %s %d across %d %s\n", + redBold("Vulnerabilities:"), + total, + len(vulns), + pluralize("package", len(vulns)), + ) + + pkgs := sortedVulnPackages(vulns) + + if verbose { + printVulnsVerbose(pkgs, vulns) + } else { + printVulnsCompact(pkgs, vulns) + } +} + +const maxVulnsPerPackage = 5 + +func printVulnsCompact( + pkgs []string, + vulns map[string][]types.Vulnerability, +) { + for _, pkg := range pkgs { + vlist := vulns[pkg] + sortVulnsBySeverity(vlist) + + fmt.Printf( + "\n %s (%d %s: %s)\n", + white(pkg), + len(vlist), + pluralize("vuln", len(vlist)), + severityBreakdown(vlist), + ) + + limit := min(maxVulnsPerPackage, len(vlist)) + for i := range limit { + printVulnLine(vlist[i]) + } + + if len(vlist) > maxVulnsPerPackage { fmt.Printf( - " %s %s %s\n", - bold(v.ID), - sevColor("["+v.Severity+"]"), - v.Summary, + " %s\n", + dim(fmt.Sprintf( + "...and %d more", + len(vlist)-maxVulnsPerPackage, + )), ) + } + } + + fmt.Printf( + "\n %s\n\n", + dim("Run angela scan -v for full details."), + ) +} + +func printVulnsVerbose( + pkgs []string, + vulns map[string][]types.Vulnerability, +) { + for _, pkg := range pkgs { + vlist := vulns[pkg] + sortVulnsBySeverity(vlist) + + fmt.Printf( + "\n %s (%d %s: %s)\n", + white(pkg), + len(vlist), + pluralize("vuln", len(vlist)), + severityBreakdown(vlist), + ) + + for _, v := range vlist { + id := preferredID(v) + sevColor := severityColorFn(v.Severity) + + fmt.Printf( + "\n %s %s\n", + sevColor( + padRight(strings.ToUpper(v.Severity), 8), + ), + bold(id), + ) + fmt.Printf(" %s\n", v.Summary) if v.FixedIn != "" { fmt.Printf( - " %s %s\n", + " %s %s\n", dim("Fixed in:"), green(v.FixedIn), ) } if v.Link != "" { - fmt.Printf( - " %s\n", - dim(v.Link), - ) + fmt.Printf(" %s\n", dim(v.Link)) } } } fmt.Println() } +func printVulnLine(v types.Vulnerability) { + id := preferredID(v) + sevColor := severityColorFn(v.Severity) + + fixedStr := "" + if v.FixedIn != "" { + fixedStr = dim("Fixed: ") + green(v.FixedIn) + } + + fmt.Printf( + " %s %s %s %s\n", + sevColor(padRight(strings.ToUpper(v.Severity), 8)), + bold(padRight(id, 16)), + truncate(v.Summary, 38), + fixedStr, + ) +} + +func sortedVulnPackages( + vulns map[string][]types.Vulnerability, +) []string { + pkgs := make([]string, 0, len(vulns)) + for k := range vulns { + pkgs = append(pkgs, k) + } + sort.Slice(pkgs, func(i, j int) bool { + li := len(vulns[pkgs[i]]) + lj := len(vulns[pkgs[j]]) + if li != lj { + return li > lj + } + return pkgs[i] < pkgs[j] + }) + return pkgs +} + +func sortVulnsBySeverity(vulns []types.Vulnerability) { + sort.Slice(vulns, func(i, j int) bool { + ri := severityRank(vulns[i].Severity) + rj := severityRank(vulns[j].Severity) + if ri != rj { + return ri < rj + } + return vulns[i].ID < vulns[j].ID + }) +} + +func severityRank(sev string) int { + switch strings.ToUpper(sev) { + case "CRITICAL": + return 0 + case "HIGH": + return 1 + case "MODERATE": + return 2 + case "LOW": + return 3 + default: + return 4 + } +} + +func severityBreakdown( + vulns []types.Vulnerability, +) string { + counts := make(map[string]int) + for _, v := range vulns { + counts[strings.ToUpper(v.Severity)]++ + } + + order := []string{ + "CRITICAL", "HIGH", "MODERATE", "LOW", "UNKNOWN", + } + + var parts []string + for _, sev := range order { + if n := counts[sev]; n > 0 { + parts = append(parts, fmt.Sprintf( + "%d %s", n, strings.ToLower(sev), + )) + } + } + return strings.Join(parts, ", ") +} + +func preferredID(v types.Vulnerability) string { + for _, alias := range v.Aliases { + if strings.HasPrefix(alias, "CVE-") { + return alias + } + } + return v.ID +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} + // PrintSummary displays final counts after an update or scan operation func PrintSummary(result types.ScanResult, updated bool) { if updated { @@ -191,7 +368,7 @@ func severityColorFn(sev string) func(a ...any) string { return redBold case "HIGH": return red - case "MEDIUM": + case "MODERATE": return yellow case "LOW": return cyan diff --git a/PROJECTS/simple-vulnerability-scanner/internal/cli/update.go b/PROJECTS/simple-vulnerability-scanner/internal/cli/update.go index 444f0585..2d279585 100644 --- a/PROJECTS/simple-vulnerability-scanner/internal/cli/update.go +++ b/PROJECTS/simple-vulnerability-scanner/internal/cli/update.go @@ -219,13 +219,13 @@ func runUpdate( } } - PrintUpdates(updates) - PrintSkipped(updates) - if vulns != nil { PrintVulnerabilities(vulns) } + PrintUpdates(updates) + PrintSkipped(updates) + totalVulns := 0 for _, vl := range vulns { totalVulns += len(vl) diff --git a/PROJECTS/simple-vulnerability-scanner/internal/osv/client.go b/PROJECTS/simple-vulnerability-scanner/internal/osv/client.go index cb1ab8aa..f0383cf6 100644 --- a/PROJECTS/simple-vulnerability-scanner/internal/osv/client.go +++ b/PROJECTS/simple-vulnerability-scanner/internal/osv/client.go @@ -359,7 +359,7 @@ func classifyScore(score float64) string { case score >= 7.0: return "HIGH" case score >= 4.0: - return "MEDIUM" + return "MODERATE" case score > 0: return "LOW" default: diff --git a/PROJECTS/simple-vulnerability-scanner/internal/osv/client_test.go b/PROJECTS/simple-vulnerability-scanner/internal/osv/client_test.go index db5b9f9e..0a3e3a8b 100644 --- a/PROJECTS/simple-vulnerability-scanner/internal/osv/client_test.go +++ b/PROJECTS/simple-vulnerability-scanner/internal/osv/client_test.go @@ -87,8 +87,8 @@ func TestClassifyScore(t *testing.T) { {9.0, "CRITICAL"}, {8.5, "HIGH"}, {7.0, "HIGH"}, - {6.9, "MEDIUM"}, - {4.0, "MEDIUM"}, + {6.9, "MODERATE"}, + {4.0, "MODERATE"}, {3.9, "LOW"}, {0.1, "LOW"}, {0.0, "NONE"},