272 lines
4.9 KiB
Go
272 lines
4.9 KiB
Go
/*
|
|
AngelaMos | 2026
|
|
finding.go
|
|
*/
|
|
|
|
package finding
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
containerIDShortLength = 12
|
|
imageIDShortLength = 12
|
|
)
|
|
|
|
type Severity int
|
|
|
|
const (
|
|
SeverityInfo Severity = iota
|
|
SeverityLow
|
|
SeverityMedium
|
|
SeverityHigh
|
|
SeverityCritical
|
|
)
|
|
|
|
var severityToString = map[Severity]string{
|
|
SeverityInfo: "INFO",
|
|
SeverityLow: "LOW",
|
|
SeverityMedium: "MEDIUM",
|
|
SeverityHigh: "HIGH",
|
|
SeverityCritical: "CRITICAL",
|
|
}
|
|
|
|
var stringToSeverity = func() map[string]Severity {
|
|
m := make(map[string]Severity, len(severityToString))
|
|
for sev, str := range severityToString {
|
|
m[str] = sev
|
|
}
|
|
return m
|
|
}()
|
|
|
|
var severityColors = map[Severity]string{
|
|
SeverityInfo: "\033[36m",
|
|
SeverityLow: "\033[34m",
|
|
SeverityMedium: "\033[33m",
|
|
SeverityHigh: "\033[31m",
|
|
SeverityCritical: "\033[35m",
|
|
}
|
|
|
|
func (s Severity) String() string {
|
|
if str, ok := severityToString[s]; ok {
|
|
return str
|
|
}
|
|
return "UNKNOWN"
|
|
}
|
|
|
|
func (s Severity) Color() string {
|
|
if color, ok := severityColors[s]; ok {
|
|
return color
|
|
}
|
|
return "\033[0m"
|
|
}
|
|
|
|
func ParseSeverity(s string) (Severity, bool) {
|
|
sev, ok := stringToSeverity[strings.ToUpper(strings.TrimSpace(s))]
|
|
return sev, ok
|
|
}
|
|
|
|
type TargetType string
|
|
|
|
const (
|
|
TargetContainer TargetType = "container"
|
|
TargetImage TargetType = "image"
|
|
TargetDockerfile TargetType = "dockerfile"
|
|
TargetCompose TargetType = "compose"
|
|
TargetDaemon TargetType = "daemon"
|
|
)
|
|
|
|
type Target struct {
|
|
Type TargetType
|
|
Name string
|
|
ID string
|
|
}
|
|
|
|
func (t Target) String() string {
|
|
if t.ID != "" && len(t.ID) >= containerIDShortLength {
|
|
return fmt.Sprintf(
|
|
"%s:%s (%s)",
|
|
t.Type,
|
|
t.Name,
|
|
t.ID[:containerIDShortLength],
|
|
)
|
|
}
|
|
if t.ID != "" {
|
|
return fmt.Sprintf("%s:%s (%s)", t.Type, t.Name, t.ID)
|
|
}
|
|
return fmt.Sprintf("%s:%s", t.Type, t.Name)
|
|
}
|
|
|
|
type Location struct {
|
|
Path string
|
|
Line int
|
|
Column int
|
|
}
|
|
|
|
func (l Location) String() string {
|
|
if l.Line > 0 {
|
|
return fmt.Sprintf("%s:%d", l.Path, l.Line)
|
|
}
|
|
return l.Path
|
|
}
|
|
|
|
type CISControl struct {
|
|
ID string
|
|
Section string
|
|
Title string
|
|
Description string
|
|
Scored bool
|
|
Level int
|
|
}
|
|
|
|
func (c CISControl) String() string {
|
|
return fmt.Sprintf("CIS %s", c.ID)
|
|
}
|
|
|
|
type Finding struct {
|
|
ID string
|
|
RuleID string
|
|
Title string
|
|
Description string
|
|
Severity Severity
|
|
Category string
|
|
Target Target
|
|
Location *Location
|
|
Remediation string
|
|
References []string
|
|
CISControl *CISControl
|
|
Timestamp time.Time
|
|
}
|
|
|
|
func New(
|
|
ruleID string,
|
|
title string,
|
|
severity Severity,
|
|
target Target,
|
|
) *Finding {
|
|
f := &Finding{
|
|
RuleID: ruleID,
|
|
Title: title,
|
|
Severity: severity,
|
|
Target: target,
|
|
Timestamp: time.Now(),
|
|
}
|
|
f.ID = f.generateID()
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) generateID() string {
|
|
data := fmt.Sprintf(
|
|
"%s|%s|%s|%s",
|
|
f.RuleID,
|
|
f.Target.Type,
|
|
f.Target.Name,
|
|
f.Target.ID,
|
|
)
|
|
if f.Location != nil {
|
|
data += fmt.Sprintf("|%s:%d", f.Location.Path, f.Location.Line)
|
|
}
|
|
hash := sha256.Sum256([]byte(data))
|
|
return hex.EncodeToString(hash[:8])
|
|
}
|
|
|
|
func (f *Finding) WithDescription(desc string) *Finding {
|
|
f.Description = desc
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) WithCategory(cat string) *Finding {
|
|
f.Category = cat
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) WithLocation(loc *Location) *Finding {
|
|
f.Location = loc
|
|
f.ID = f.generateID()
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) WithRemediation(rem string) *Finding {
|
|
f.Remediation = rem
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) WithReferences(refs ...string) *Finding {
|
|
f.References = refs
|
|
return f
|
|
}
|
|
|
|
func (f *Finding) WithCISControl(control *CISControl) *Finding {
|
|
f.CISControl = control
|
|
return f
|
|
}
|
|
|
|
// Collection is a slice of findings with filtering and aggregation methods
|
|
type Collection []*Finding
|
|
|
|
func (c Collection) BySeverity(sev Severity) Collection {
|
|
result := make(Collection, 0, len(c))
|
|
for _, f := range c {
|
|
if f.Severity == sev {
|
|
result = append(result, f)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c Collection) AtOrAbove(sev Severity) Collection {
|
|
result := make(Collection, 0, len(c))
|
|
for _, f := range c {
|
|
if f.Severity >= sev {
|
|
result = append(result, f)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c Collection) ByCategory(cat string) Collection {
|
|
result := make(Collection, 0, len(c))
|
|
for _, f := range c {
|
|
if f.Category == cat {
|
|
result = append(result, f)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c Collection) ByTargetType(tt TargetType) Collection {
|
|
result := make(Collection, 0, len(c))
|
|
for _, f := range c {
|
|
if f.Target.Type == tt {
|
|
result = append(result, f)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c Collection) CountBySeverity() map[Severity]int {
|
|
counts := make(map[Severity]int, len(severityToString))
|
|
for _, f := range c {
|
|
counts[f.Severity]++
|
|
}
|
|
return counts
|
|
}
|
|
|
|
func (c Collection) HasSeverityAtOrAbove(sev Severity) bool {
|
|
for _, f := range c {
|
|
if f.Severity >= sev {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c Collection) Total() int {
|
|
return len(c)
|
|
}
|