198 lines
3.0 KiB
Go
198 lines
3.0 KiB
Go
// ©AngelaMos | 2026
|
|
// format.go
|
|
|
|
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/CarterPerez-dev/nadezhda/internal/store"
|
|
)
|
|
|
|
type band int
|
|
|
|
const (
|
|
bandNone band = iota
|
|
bandLow
|
|
bandMedium
|
|
bandHigh
|
|
bandCritical
|
|
)
|
|
|
|
const (
|
|
cvssCritical = 9.0
|
|
cvssHigh = 7.0
|
|
cvssMedium = 4.0
|
|
|
|
epssHot = 0.5
|
|
epssWarm = 0.1
|
|
|
|
secsPerMinute = 60
|
|
secsPerHour = 3600
|
|
secsPerDay = 86400
|
|
|
|
ellipsis = "…"
|
|
emptyMarker = "───"
|
|
naMarker = "—"
|
|
)
|
|
|
|
func clamp01(v float64) float64 {
|
|
if v < 0 {
|
|
return 0
|
|
}
|
|
if v > 1 {
|
|
return 1
|
|
}
|
|
return v
|
|
}
|
|
|
|
func cvssBand(score *float64) band {
|
|
if score == nil {
|
|
return bandNone
|
|
}
|
|
switch v := *score; {
|
|
case v >= cvssCritical:
|
|
return bandCritical
|
|
case v >= cvssHigh:
|
|
return bandHigh
|
|
case v >= cvssMedium:
|
|
return bandMedium
|
|
case v > 0:
|
|
return bandLow
|
|
default:
|
|
return bandNone
|
|
}
|
|
}
|
|
|
|
func bandLabel(b band) string {
|
|
switch b {
|
|
case bandCritical:
|
|
return "CRITICAL"
|
|
case bandHigh:
|
|
return "HIGH"
|
|
case bandMedium:
|
|
return "MEDIUM"
|
|
case bandLow:
|
|
return "LOW"
|
|
default:
|
|
return naMarker
|
|
}
|
|
}
|
|
|
|
func clusterHasKEV(c store.DigestCluster) bool {
|
|
for _, v := range c.CVEs {
|
|
if v.IsKEV {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func clusterMaxCVSS(c store.DigestCluster) *float64 {
|
|
var best *float64
|
|
for _, v := range c.CVEs {
|
|
if v.CVSSScore == nil {
|
|
continue
|
|
}
|
|
if best == nil || *v.CVSSScore > *best {
|
|
best = v.CVSSScore
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func clusterMaxEPSS(c store.DigestCluster) *float64 {
|
|
var best *float64
|
|
for _, v := range c.CVEs {
|
|
if v.EPSS == nil {
|
|
continue
|
|
}
|
|
if best == nil || *v.EPSS > *best {
|
|
best = v.EPSS
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func clusterBand(c store.DigestCluster) band {
|
|
if clusterHasKEV(c) {
|
|
return bandCritical
|
|
}
|
|
return cvssBand(clusterMaxCVSS(c))
|
|
}
|
|
|
|
func epssBand(p *float64) band {
|
|
if p == nil {
|
|
return bandNone
|
|
}
|
|
switch v := *p; {
|
|
case v >= epssHot:
|
|
return bandCritical
|
|
case v >= epssWarm:
|
|
return bandHigh
|
|
case v > 0:
|
|
return bandMedium
|
|
default:
|
|
return bandNone
|
|
}
|
|
}
|
|
|
|
func cvssString(score *float64) string {
|
|
if score == nil {
|
|
return naMarker
|
|
}
|
|
return fmt.Sprintf("%.1f", *score)
|
|
}
|
|
|
|
func epssString(p *float64) string {
|
|
if p == nil {
|
|
return naMarker
|
|
}
|
|
return fmt.Sprintf("%.1f%%", clamp01(*p)*100)
|
|
}
|
|
|
|
func relativeAge(unix int64, now time.Time) string {
|
|
if unix <= 0 {
|
|
return naMarker
|
|
}
|
|
secs := now.Unix() - unix
|
|
if secs < 0 {
|
|
secs = 0
|
|
}
|
|
switch {
|
|
case secs < secsPerMinute:
|
|
return "just now"
|
|
case secs < secsPerHour:
|
|
return fmt.Sprintf("%dm ago", secs/secsPerMinute)
|
|
case secs < secsPerDay:
|
|
return fmt.Sprintf("%dh ago", secs/secsPerHour)
|
|
default:
|
|
return fmt.Sprintf("%dd ago", secs/secsPerDay)
|
|
}
|
|
}
|
|
|
|
func truncate(s string, max int) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
r := []rune(s)
|
|
if max <= 0 {
|
|
return ""
|
|
}
|
|
if len(r) <= max {
|
|
return s
|
|
}
|
|
if max == 1 {
|
|
return ellipsis
|
|
}
|
|
return string(r[:max-1]) + ellipsis
|
|
}
|
|
|
|
func padRight(s string, width int) string {
|
|
w := len([]rune(s))
|
|
if w >= width {
|
|
return s
|
|
}
|
|
return s + strings.Repeat(" ", width-w)
|
|
}
|