Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/finding/finding.go

295 lines
5.8 KiB
Go

/*
©AngelaMos | 2026
finding.go
Core types for security findings produced by all analyzers
Finding is the central data type flowing through the entire pipeline.
Severity is an ordered enum (Info through Critical) with color
support for terminal output. Collection provides filtering and
aggregation over a slice of findings and is the return type of
every Analyze() call.
Key exports:
Finding - single security issue with rule ID, severity, target, and
location
Collection - slice of findings with BySeverity, AtOrAbove,
CountBySeverity
Severity - ordered enum Info < Low < Medium < High < Critical
Target, Location, CISControl - embedded context types
Connects to:
config.go - severity parsing and filter logic
rules/*.go - severity constants for capability/path classification
analyzer/*.go - findings created and returned as Collection
report/*.go - findings consumed by all four reporters
benchmark/controls.go - CISControl linked to findings via WithCISControl
*/
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)
}