219 lines
4.5 KiB
Go
219 lines
4.5 KiB
Go
// ©AngelaMos | 2026
|
|
// output.go
|
|
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/CarterPerez-dev/angela/internal/pypi"
|
|
"github.com/CarterPerez-dev/angela/pkg/types"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
var (
|
|
bold = color.New(color.Bold).SprintFunc()
|
|
dim = color.New(color.Faint).SprintFunc()
|
|
cyan = color.New(color.FgCyan, color.Bold).SprintFunc()
|
|
green = color.New(color.FgGreen).SprintFunc()
|
|
yellow = color.New(color.FgYellow).SprintFunc()
|
|
red = color.New(color.FgRed).SprintFunc()
|
|
redBold = color.New(color.FgRed, color.Bold).SprintFunc()
|
|
greenBold = color.New(color.FgGreen, color.Bold).SprintFunc()
|
|
white = color.New(color.FgWhite, color.Bold).SprintFunc()
|
|
)
|
|
|
|
// PrintScanning announces the start of a scan
|
|
func PrintScanning(count int) {
|
|
fmt.Printf(
|
|
"\n %s %d dependencies...\n\n",
|
|
cyan("Scanning"), count,
|
|
)
|
|
}
|
|
|
|
// PrintUpdates displays a formatted table of available dependency updates
|
|
func PrintUpdates(updates []types.UpdateResult) {
|
|
var actionable []types.UpdateResult
|
|
for _, u := range updates {
|
|
if !u.Skipped {
|
|
actionable = append(actionable, u)
|
|
}
|
|
}
|
|
|
|
if len(actionable) == 0 {
|
|
fmt.Printf(" %s\n\n", dim("All dependencies are up to date."))
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" %s\n", cyan("Updates available:"))
|
|
|
|
nameWidth := 0
|
|
oldWidth := 0
|
|
for _, u := range actionable {
|
|
if len(u.Name) > nameWidth {
|
|
nameWidth = len(u.Name)
|
|
}
|
|
if len(u.OldVer) > oldWidth {
|
|
oldWidth = len(u.OldVer)
|
|
}
|
|
}
|
|
|
|
for _, u := range actionable {
|
|
changeColor := changeColorFn(u.Change)
|
|
fmt.Printf(
|
|
" %-*s %s %s %s %s\n",
|
|
nameWidth, white(u.Name),
|
|
dim(padRight(u.OldVer, oldWidth)),
|
|
dim("->"),
|
|
changeColor(u.NewVer),
|
|
dim("("+u.Change+")"),
|
|
)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// PrintSkipped displays dependencies that were not updated with reasons
|
|
func PrintSkipped(updates []types.UpdateResult) {
|
|
var skipped []types.UpdateResult
|
|
for _, u := range updates {
|
|
if u.Skipped {
|
|
skipped = append(skipped, u)
|
|
}
|
|
}
|
|
if len(skipped) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" %s\n", dim("Skipped:"))
|
|
for _, u := range skipped {
|
|
fmt.Printf(" %s %s\n", dim(u.Name), dim(u.Reason))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// PrintVulnerabilities displays a formatted list of security advisories
|
|
func PrintVulnerabilities(
|
|
vulns map[string][]types.Vulnerability,
|
|
) {
|
|
if len(vulns) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" %s\n", redBold("Vulnerabilities found:"))
|
|
|
|
for pkg, vlist := range vulns {
|
|
fmt.Printf(" %s\n", white(pkg))
|
|
for _, v := range vlist {
|
|
sevColor := severityColorFn(v.Severity)
|
|
fmt.Printf(
|
|
" %s %s %s\n",
|
|
bold(v.ID),
|
|
sevColor("["+v.Severity+"]"),
|
|
v.Summary,
|
|
)
|
|
if v.FixedIn != "" {
|
|
fmt.Printf(
|
|
" %s %s\n",
|
|
dim("Fixed in:"),
|
|
green(v.FixedIn),
|
|
)
|
|
}
|
|
if v.Link != "" {
|
|
fmt.Printf(
|
|
" %s\n",
|
|
dim(v.Link),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// PrintSummary displays final counts after an update or scan operation
|
|
func PrintSummary(result types.ScanResult, updated bool) {
|
|
if updated {
|
|
fmt.Printf(" %s\n", greenBold("Updated pyproject.toml"))
|
|
}
|
|
fmt.Printf(
|
|
" %s packages checked\n",
|
|
bold(fmt.Sprintf("%d", result.TotalPackages)),
|
|
)
|
|
if result.TotalUpdated > 0 {
|
|
fmt.Printf(
|
|
" %s updated\n",
|
|
bold(fmt.Sprintf("%d", result.TotalUpdated)),
|
|
)
|
|
}
|
|
if result.VulnsScanned {
|
|
if result.TotalVulns > 0 {
|
|
fmt.Printf(
|
|
" %s\n",
|
|
red(fmt.Sprintf(
|
|
"%d %s found",
|
|
result.TotalVulns,
|
|
pluralize("vulnerability", result.TotalVulns),
|
|
)),
|
|
)
|
|
} else {
|
|
fmt.Printf(
|
|
" %s\n",
|
|
green("No vulnerabilities found"),
|
|
)
|
|
}
|
|
}
|
|
fmt.Printf(
|
|
" %s %s\n\n",
|
|
dim("Done in"),
|
|
dim(result.Duration.Round(1e6).String()),
|
|
)
|
|
}
|
|
|
|
// PrintError displays a user-friendly error message
|
|
func PrintError(msg string) {
|
|
fmt.Printf("\n %s %s\n\n", red("error:"), msg)
|
|
}
|
|
|
|
func changeColorFn(kind string) func(a ...any) string {
|
|
switch kind {
|
|
case pypi.Major.String():
|
|
return red
|
|
case pypi.Minor.String():
|
|
return yellow
|
|
default:
|
|
return green
|
|
}
|
|
}
|
|
|
|
func severityColorFn(sev string) func(a ...any) string {
|
|
switch strings.ToUpper(sev) {
|
|
case "CRITICAL":
|
|
return redBold
|
|
case "HIGH":
|
|
return red
|
|
case "MEDIUM":
|
|
return yellow
|
|
case "LOW":
|
|
return cyan
|
|
default:
|
|
return dim
|
|
}
|
|
}
|
|
|
|
func padRight(s string, width int) string {
|
|
if len(s) >= width {
|
|
return s
|
|
}
|
|
return s + strings.Repeat(" ", width-len(s))
|
|
}
|
|
|
|
func pluralize(word string, n int) string {
|
|
if n == 1 {
|
|
return word
|
|
}
|
|
if strings.HasSuffix(word, "y") {
|
|
return word[:len(word)-1] + "ies"
|
|
}
|
|
return word + "s"
|
|
}
|