Cybersecurity-Projects/PROJECTS/simple-vulnerability-scanner/internal/cli/output.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"
}