overhaul vuln output with compact/verbose modes, reorder vulns before updates

This commit is contained in:
CarterPerez-dev 2026-01-28 00:23:43 -05:00
parent 4c65756ba9
commit c9cf020de4
4 changed files with 199 additions and 22 deletions

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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"},