// 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 }) }