overhaul vuln output with compact/verbose modes, reorder vulns before updates
This commit is contained in:
parent
4c65756ba9
commit
c9cf020de4
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
Loading…
Reference in New Issue