pkg/checks/checks.go
Rama main · réplica de lectura del repositorio original.
// Package checks is the shared, dependency-light model behind the "Checks"
// remediation queue. It groups effective findings by check into prioritized
// rows (Check) — one implementation used by both OSS Radar (single cluster)
// and Radar Hub (fleet) so the two surfaces can't drift.
//
// It is deliberately k8s-free (stdlib only): Radar Hub imports it without
// pulling k8s.io type libraries into a service that never touches Kubernetes.
// The k8s-aware audit engine (pkg/audit) layers on top — its CheckMeta is an
// alias of this package's, and it provides the raw->effective converter.
package checks
import (
"sort"
"github.com/skyhook-io/radar/pkg/resourceid"
)
// Severity is the canonical user-facing Checks severity ladder. Live issues
// remain a separate operational-health axis even when both domains use a
// familiar severity word.
type Severity string
const (
SeverityCritical Severity = "critical"
SeverityHigh Severity = "high"
SeverityMedium Severity = "medium"
SeverityLow Severity = "low"
)
// Raw detector severities remain in scanner provenance and existing audit
// transport/public-property compatibility contracts (mirrors pkg/audit).
// Checks and agent-facing surfaces map them before exposure.
const (
rawDanger = "danger"
rawWarning = "warning"
)
// MapSeverity maps a raw detector severity to the Checks ladder: danger->high,
// warning->medium. critical/low are only reachable via an org severity override
// (Hub) — the detector never emits them directly. Unknown inputs fall to medium.
func MapSeverity(raw string) Severity {
switch raw {
case rawDanger:
return SeverityHigh
case rawWarning:
return SeverityMedium
default:
return SeverityMedium
}
}
// ValidSeverity reports whether s is one of the four ladder values.
func ValidSeverity(s string) bool {
switch Severity(s) {
case SeverityCritical, SeverityHigh, SeverityMedium, SeverityLow:
return true
}
return false
}
// SeverityRank orders the ladder for "worst first" sorting.
func SeverityRank(s Severity) int {
switch s {
case SeverityCritical:
return 4
case SeverityHigh:
return 3
case SeverityMedium:
return 2
case SeverityLow:
return 1
default:
return 0
}
}
// SourceRadarBuiltin is the only finding source in V1: Radar's built-in
// detectors. External detectors (Trivy, Polaris, …) would add more later.
const SourceRadarBuiltin = "radar_builtin"
// CheckMeta is a check's static definition (catalog entry). pkg/audit aliases
// this type so the audit engine's registry and this package share one shape.
type CheckMeta struct {
ID string `json:"id"`
Title string `json:"title"`
Category string `json:"category,omitempty"` // Security|Reliability|Efficiency — the bucket findings emit under
// DefaultSeverity is the Checks-ladder level (MapSeverity of the detector's
// raw severity) a finding carries with no org override — its common/base
// case for checks that ramp or split by context. May be empty for catalogs
// from a Radar predating the field; consumers fall back to "Default".
DefaultSeverity string `json:"defaultSeverity,omitempty"`
Description string `json:"description"`
Remediation string `json:"remediation"`
Frameworks []string `json:"frameworks,omitempty"`
References []Reference `json:"references,omitempty"`
// BadgeWorthy marks a check whose finding means the specific resource is
// actually broken (reference-integrity, lifecycle, will-break-on-upgrade) —
// worth a per-resource badge on the topology graph and resource list.
// Deliberately FALSE for security-posture and best-practice checks (runAsRoot,
// missing limits, single-replica…) that fire on nearly every resource and
// would turn the badges into noise; those stay in the Checks/Audit views.
BadgeWorthy bool `json:"badgeWorthy,omitempty"`
}
// Reference is an authoritative link for a check (Kubernetes docs, CIS,
// NSA/CISA, …), surfaced in the expanded card so users can go deeper than the
// brief inline copy.
type Reference struct {
Label string `json:"label"`
URL string `json:"url"`
}
// ResourceRef is the canonical resource identity. group/namespace are emitted
// even when empty (core group / cluster-scoped) so consumers never optional-
// chain. ClusterID scopes the ref to its source cluster — the disambiguator
// when display names collapse across clusters in the fleet view ("" for OSS
// single-cluster).
type ResourceRef struct {
ClusterID string `json:"cluster_id"`
Group string `json:"group"`
Kind string `json:"kind"`
Namespace string `json:"namespace"`
Name string `json:"name"`
}
// EffectiveFindingState explains how policy shaped a finding. Visibility is
// always "visible" for findings present in the effective output (hidden ones
// are dropped upstream); the field exists for forward-compat with a config view
// that surfaces hidden rows. Source distinguishes detector defaults from
// config-driven outcomes (a severity override).
type EffectiveFindingState struct {
Visibility string `json:"visibility"` // visible|hidden
Source string `json:"source"` // detector_default|org_config
ScoreImpact string `json:"scoreImpact"` // counts|excluded
AlertImpact string `json:"alertImpact"` // alerts|muted
ComplianceImpact string `json:"complianceImpact"` // counts|excluded_by_config
Reason string `json:"reason,omitempty"`
}
// DefaultEffectiveState is the detector-default state — no policy shaping. The
// OSS path uses this for every finding (local settings only drop findings, they
// never override severity); the Hub overrides Source/Reason when org config
// changes a finding's effective severity.
func DefaultEffectiveState() EffectiveFindingState {
return EffectiveFindingState{
Visibility: "visible",
Source: "detector_default",
ScoreImpact: "counts",
AlertImpact: "alerts",
ComplianceImpact: "counts",
}
}
// EffectiveFinding is one per-resource check failure, carrying both the raw
// detector severity (OriginalSeverity) and the resolved ladder value
// (EffectiveSeverity) plus the state explaining any policy shaping.
type EffectiveFinding struct {
Source string `json:"source"`
Resource ResourceRef `json:"resource"`
CheckID string `json:"checkID"`
Category string `json:"category"`
OriginalSeverity string `json:"originalSeverity"`
EffectiveSeverity Severity `json:"effectiveSeverity"`
Message string `json:"message"`
State EffectiveFindingState `json:"state"`
}
// Check is one row of the remediation queue: a single failing check, rolling up
// every resource that fails it. Subject is the most-severe representative
// resource; Findings holds the per-resource detail underneath. (Distinct from
// CheckMeta, which is the check's static definition.)
type Check struct {
ID string `json:"id"`
Source string `json:"source"`
Subject ResourceRef `json:"subject"`
CheckID string `json:"checkID"`
Category string `json:"category"`
EffectiveSeverity Severity `json:"effectiveSeverity"`
Title string `json:"title"`
Message string `json:"message"`
AffectedFindings int `json:"affectedFindings"`
AffectedResources int `json:"affectedResources"`
RepresentativeFinding EffectiveFinding `json:"representativeFinding"`
Findings []EffectiveFinding `json:"findings"`
// Environment is the source cluster's environment label (e.g. "prod"),
// surfaced in the UI as a context tag. Empty for OSS single-cluster (no
// fleet environment) and for unlabeled clusters.
Environment string `json:"environment,omitempty"`
}
// Categories — mirrors pkg/audit's category vocabulary. Kept here so the
// priority weights are self-contained.
const (
CategorySecurity = "Security"
CategoryReliability = "Reliability"
CategoryEfficiency = "Efficiency"
)
// BuildChecks groups effective findings by checkID into the remediation queue:
// one Check per failing check, aggregating every resource that fails it. The
// subject is the highest-severity representative resource. Ordering is
// deterministic and worst-first — severity, then blast radius (affected
// resources), then checkID — matching the rendered queue; each Check's member
// findings are also sorted worst-first. clusterID scopes the row ID; env (may
// be "") is surfaced as the check's Environment context tag.
func BuildChecks(findings []EffectiveFinding, catalog map[string]CheckMeta, clusterID, env string) []Check {
type bucket struct {
findings []EffectiveFinding
resKeys map[string]bool
}
buckets := map[string]*bucket{}
order := []string{}
for _, f := range findings {
b := buckets[f.CheckID]
if b == nil {
b = &bucket{resKeys: map[string]bool{}}
buckets[f.CheckID] = b
order = append(order, f.CheckID)
}
b.findings = append(b.findings, f)
b.resKeys[refKey(f.Resource)] = true
}
checks := make([]Check, 0, len(order))
for _, checkID := range order {
b := buckets[checkID]
// Order member findings worst-first so the drawer reads top-down by
// severity and findings[0] is the most-severe representative.
sortEffectiveFindings(b.findings)
rep := b.findings[0]
worst := rep.EffectiveSeverity
meta := catalog[checkID]
title := meta.Title
if title == "" {
title = checkID
}
message := rep.Message
if message == "" {
message = meta.Description
}
checks = append(checks, Check{
ID: clusterID + "|" + SourceRadarBuiltin + "|" + checkID,
Source: SourceRadarBuiltin,
Subject: rep.Resource,
CheckID: checkID,
Category: rep.Category,
EffectiveSeverity: worst,
Title: title,
Message: message,
AffectedFindings: len(b.findings),
AffectedResources: len(b.resKeys),
RepresentativeFinding: rep,
Findings: b.findings,
Environment: env,
})
}
// Worst-first, matching the rendered queue: severity, then blast radius,
// then checkID for stability. Consumers re-group/re-sort for display; this
// is the canonical default order.
sort.SliceStable(checks, func(i, j int) bool {
ri, rj := SeverityRank(checks[i].EffectiveSeverity), SeverityRank(checks[j].EffectiveSeverity)
if ri != rj {
return ri > rj
}
if checks[i].AffectedResources != checks[j].AffectedResources {
return checks[i].AffectedResources > checks[j].AffectedResources
}
return checks[i].CheckID < checks[j].CheckID
})
return checks
}
// refKey dedups resources within a check. ClusterID is constant per
// BuildChecks call, so it isn't part of the intra-check key.
func refKey(r ResourceRef) string {
return resourceid.ResourceKey(r.Group, r.Kind, r.Namespace, r.Name)
}
func sortEffectiveFindings(findings []EffectiveFinding) {
sort.SliceStable(findings, func(i, j int) bool {
ri, rj := SeverityRank(findings[i].EffectiveSeverity), SeverityRank(findings[j].EffectiveSeverity)
if ri != rj {
return ri > rj
}
if findings[i].Resource.Kind != findings[j].Resource.Kind {
return findings[i].Resource.Kind < findings[j].Resource.Kind
}
if findings[i].Resource.Namespace != findings[j].Resource.Namespace {
return findings[i].Resource.Namespace < findings[j].Resource.Namespace
}
return findings[i].Resource.Name < findings[j].Resource.Name
})
}