internal/checks/checks.go
Rama main · réplica de lectura del repositorio original.
package checks
import (
"context"
"encoding/base64"
"errors"
"strings"
"time"
"k8s.io/pod-security-admission/policy"
"github.com/sophotechlabs/spinoza/internal/api"
)
var ErrNoSuchCheck = errors.New("no check goes by that name")
const (
severityHigh = api.SeverityHigh
severityMedium = api.SeverityMedium
severityLow = api.SeverityLow
categorySecurity = "security"
categoryReliability = "reliability"
categoryEfficiency = "efficiency"
pssBaseline = "PSS baseline"
pssRestricted = "PSS restricted"
nsaCisa = "NSA/CISA"
cisBenchmark = "CIS Kubernetes Benchmark"
noUsage = "metrics-server did not answer, so usage is unknown"
notEveryKind = "not audited: this decides what nothing references, " +
"which is only true once every kind has been read"
readsPart = "Nothing among the kinds this read names it. It read every custom resource, " +
"the workload, config and storage kinds, and the built-in kinds that hold references to them " +
"— not every built-in kind. Something outside the cluster still might name it, " +
"which is the one thing this cannot see."
readsEvery = "Nothing anywhere in the cluster names it. Every kind the cluster reports was read " +
"to decide this. Something outside the cluster still might, which is the one thing this cannot see."
findingsShown = 200
)
type scan struct {
subjects []Subject
usage map[string]api.ResourceUsage
held *corpus
facts Facts
upstream *upstreamScan
faults *ruleDiagnostics
everyKind bool
custom bool
}
func (sc scan) hasUsage() bool {
return len(sc.usage) > 0
}
type found struct {
subject Subject
container string
detail string
patch string
convention string
severity string
unmatched bool
}
type objects struct {
index map[string]int
list []api.CheckObject
}
func newObjects() *objects {
return &objects{index: map[string]int{}}
}
func (o *objects) ref(subject Subject) int {
key := subject.Kind + "/" + subject.Ref.Namespace + "/" + subject.Ref.Name
at, seen := o.index[key]
if seen {
return at
}
at = len(o.list)
o.index[key] = at
o.list = append(o.list, api.CheckObject{
Group: subject.Ref.Group,
Version: subject.Ref.Version,
Resource: subject.Ref.Resource,
Namespace: subject.Ref.Namespace,
Name: subject.Ref.Name,
Kind: subject.Kind,
Origin: subject.Origin,
ManagedBy: subject.ManagedBy,
})
return at
}
type finder func(scan) []found
type containerRule func(Subject, Container) (string, string)
type subjectRule func(Subject) (string, string)
type usageRule func(Subject, map[string]api.ResourceUsage) (string, string)
type factRule func(Subject, *corpus) (string, string)
type check struct {
id string
upstream policy.CheckID
presented func(Subject) bool
enforced bool
title string
category string
severity string
frameworks []string
wrong string
wrongEvery string
remedy string
needsUsage bool
needsEvery bool
arguable bool
needs []target
imported *importOrigin
find finder
}
func overContainers(rule containerRule) finder {
return func(sc scan) []found {
out := []found{}
for _, subject := range sc.subjects {
for _, container := range subject.Containers {
detail, patch := rule(subject, container)
if detail == "" {
continue
}
out = append(out, found{
subject: subject,
container: container.Name,
detail: detail,
patch: patch,
})
}
}
return out
}
}
func overSubjects(rule subjectRule) finder {
return func(sc scan) []found {
out := []found{}
for _, subject := range sc.subjects {
detail, patch := rule(subject)
if detail == "" {
continue
}
out = append(out, found{subject: subject, detail: detail, patch: patch})
}
return out
}
}
func overFacts(rule factRule) finder {
return func(sc scan) []found {
out := []found{}
for _, subject := range sc.subjects {
detail, patch := rule(subject, sc.held)
if detail == "" {
continue
}
out = append(out, found{subject: subject, detail: detail, patch: patch})
}
return out
}
}
func overUsage(rule usageRule) finder {
return func(sc scan) []found {
out := []found{}
for _, subject := range sc.subjects {
detail, patch := rule(subject, sc.usage)
if detail == "" {
continue
}
out = append(out, found{subject: subject, detail: detail, patch: patch})
}
return out
}
}
func findingKey(item found) string {
return rankOf(item.severity) + "\x00" + subjectKey(item.subject) + "\x00" + item.container
}
func encodeCursor(key string) string {
if key == "" {
return ""
}
return base64.RawURLEncoding.EncodeToString([]byte(key))
}
func decodeCursor(cursor string) string {
if cursor == "" {
return ""
}
raw, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
return ""
}
return string(raw)
}
type marked struct {
found
muted bool
by Mute
fresh bool
byRule bool
}
type tally struct {
found int
total int
muted int
fresh int
here map[string]bool
}
func (c check) matching(sc scan, keep Filter) ([]marked, tally) {
all := c.ranked(c.findings(sc))
out := make([]marked, 0, len(all))
count := tally{}
here := map[string]bool{}
for _, item := range all {
if !keep.keeps(item) {
continue
}
count.found++
key := c.id + "\x00" + fingerprintOf(identityOf(c.id, item))
here[key] = true
by, muted := keep.mutes(c.id, item)
rule := false
if !muted {
by, muted = keep.silenced(c.id, item, sc.held)
rule = muted
}
if !muted && item.convention != "" {
by, muted = Mute{Check: c.id, Reason: item.convention}, true
}
if muted {
count.muted++
if !keep.ShowMuted {
continue
}
}
fresh := c.comparable() && !keep.foreign() && keep.Base.covers(c.id) && !keep.Base.has(key)
if fresh {
count.fresh++
}
if keep.OnlyNew && !fresh {
continue
}
out = append(out, marked{found: item, muted: muted, by: by, fresh: fresh, byRule: rule})
}
count.total = len(out)
count.here = here
return out, count
}
func mutedBy(item marked) string {
if !item.muted {
return ""
}
if item.byRule {
return ScopeRule
}
if item.convention != "" {
return ScopeConvention
}
return scopeOf(item.by)
}
func page(all []marked, objs *objects, after string, limit int) ([]api.CheckFinding, string) {
out := make([]api.CheckFinding, 0, min(limit, len(all)))
last := ""
for _, item := range all {
key := findingKey(item.found)
if key <= after {
continue
}
if len(out) == limit {
return out, encodeCursor(last)
}
out = append(out, api.CheckFinding{
Ref: objs.ref(item.subject),
Container: item.container,
Detail: item.detail,
Patch: item.patch,
Severity: item.severity,
New: item.fresh,
Muted: item.muted,
MutedBy: mutedBy(item),
Reason: item.by.Reason,
Unmatched: item.unmatched,
})
last = key
}
return out, ""
}
func wantsCorpus(keep Filter) bool {
for _, entry := range keep.chosen(keep.checks()) {
if entry.needsEvery {
return true
}
}
return false
}
func (c check) comparable() bool {
return !c.needsUsage
}
func (c check) standsDown(sc scan) string {
if c.needsUsage && !sc.hasUsage() {
return noUsage
}
if c.needsEvery {
if !sc.custom {
return notEveryKind
}
if refused := sc.held.refused(); refused != "" {
return "not audited: the cluster would not let this read " + refused +
", and what nothing references cannot be decided from part of it"
}
}
if missing := missingResources(c.needs, sc.held); len(missing) > 0 {
return skippedBecause(missing)
}
if c.upstream != "" {
if down := sc.upstream.standsDown(c.upstream); down != "" {
return down
}
}
if c.enforced && sc.upstream.err != nil {
return upstreamUnloaded + sc.upstream.err.Error()
}
return ""
}
func (c check) group(sc scan, objs *objects, spread *namespaces, keep Filter, shown int) api.CheckGroup {
out := api.CheckGroup{
ID: c.id,
Title: c.title,
Category: c.category,
Severity: c.severity,
Frameworks: c.frameworksFor(sc),
Wrong: c.wrong,
Remedy: c.remedy,
Findings: []api.CheckFinding{},
}
if sc.everyKind && c.wrongEvery != "" {
out.Wrong = c.wrongEvery
}
if c.imported != nil {
out.Sources = c.imported.sources
out.Taken = c.imported.taken.UTC().Format(time.RFC3339)
}
if down := c.standsDown(sc); down != "" {
out.Skipped = down
return out
}
all, count := c.matching(sc, keep)
out.Findings, out.Next = page(all, objs, "", shown)
out.Total, out.Muted, out.NewCount = count.total, count.muted, count.fresh
if c.comparable() {
out.Fixed = keep.fixedSince(c.id, count.found)
out.Gone = keep.goneSince(c.id, count.here)
out.Baselined = keep.Base.covers(c.id)
if was, ran := keep.countedBefore(c.id); ran {
out.Was, out.Ran = was, true
}
}
out.Measured = !c.comparable()
out.Truncated = out.Next != ""
spread.add(all)
return out
}
func joined(parts ...string) string {
kept := []string{}
for _, part := range parts {
if part == "" {
continue
}
kept = append(kept, part)
}
return strings.Join(kept, "; ")
}
func registryWith(rules []UserRule, files []importFile) []check {
out := append(registry(), userChecks(rules)...)
return append(out, importedChecks(files)...)
}
func registryFor(keep Filter) ([]check, []string) {
files, faults := readImports(keep.Imports)
return registryWith(keep.Rules, files), faults
}
func (f Filter) checks() []check {
out, _ := registryFor(f)
return out
}
func registry() []check {
out := securityChecks()
out = append(out, hardeningChecks()...)
out = append(out, reliabilityChecks()...)
out = append(out, lifecycleChecks()...)
out = append(out, supplyChecks()...)
out = append(out, batchChecks()...)
out = append(out, efficiencyChecks()...)
out = append(out, factChecks()...)
out = append(out, referenceChecks()...)
out = append(out, rbacChecks()...)
out = append(out, objectChecks()...)
out = append(out, deprecationChecks()...)
return out
}
func Page(
ctx context.Context,
lister Lister,
descs map[string]api.ResourceDescriptor,
usage api.Metrics,
id string,
after string,
keep Filter,
shown int,
) (api.CheckPage, error) {
return (*Surveys)(nil).Page(ctx, lister, descs, usage, id, after, keep, shown)
}
func (s *Surveys) Page(
ctx context.Context,
lister Lister,
descs map[string]api.ResourceDescriptor,
usage api.Metrics,
id string,
after string,
keep Filter,
shown int,
) (api.CheckPage, error) {
var wanted check
for _, entry := range keep.checks() {
if entry.id == id {
wanted = entry
}
}
if wanted.id == "" {
return api.CheckPage{}, ErrNoSuchCheck
}
sc, _, _ := s.take(ctx, lister, descs, usage, keep)
if wanted.standsDown(sc) != "" {
return api.CheckPage{Findings: []api.CheckFinding{}, Objects: []api.CheckObject{}}, nil
}
objs := newObjects()
all, _ := wanted.matching(sc, keep)
found, next := page(all, objs, decodeCursor(after), pageSize(shown))
return api.CheckPage{Findings: found, Objects: objs.list, Next: next}, nil
}
func pageSize(shown int) int {
if shown <= 0 {
return findingsShown
}
return shown
}
func survey(
ctx context.Context,
lister Lister,
descs map[string]api.ResourceDescriptor,
usage api.Metrics,
keep Filter,
) (scan, string, []string) {
wanted, absent := needed(descs, keep.WholeCluster)
custom := false
if keep.EveryKind {
wanted, absent, custom = everyDiscovered(descs), nil, true
}
if keep.WholeCluster && !keep.EveryKind {
if wantsCorpus(keep) {
wanted = append(wanted, customKinds(descs)...)
wanted = append(wanted, mentionKinds(descs)...)
custom = true
}
wanted = append(wanted, alsoWarm(lister, wanted)...)
}
items, names, unread, mentions, failure := gather(ctx, lister, wanted)
facts := lister.Facts()
return scan{
subjects: subjectsOf(items),
usage: usage.Pods,
held: newCorpus(items, names, absent, targetsFor(keep.WholeCluster), unread, mentions),
facts: facts,
upstream: newUpstreamScan(facts.ServerVersion),
everyKind: keep.EveryKind,
custom: custom,
}, failure, absent
}
func Run(
ctx context.Context,
lister Lister,
descs map[string]api.ResourceDescriptor,
usage api.Metrics,
keep Filter,
shown int,
) api.CheckReport {
return (*Surveys)(nil).Run(ctx, lister, descs, usage, keep, shown)
}
func (s *Surveys) Run(
ctx context.Context,
lister Lister,
descs map[string]api.ResourceDescriptor,
usage api.Metrics,
keep Filter,
shown int,
) api.CheckReport {
sc, failure, absent := s.take(ctx, lister, descs, usage, keep)
keep.ruleFailures = newRuleDiagnostics()
sc.faults = keep.ruleFailures
registered, importFaults := registryFor(keep)
checks := keep.chosen(registered)
objs := newObjects()
spread := newNamespaces()
groups := make([]api.CheckGroup, 0, len(checks))
for _, entry := range checks {
if ctx.Err() != nil {
return api.CheckReport{
Groups: []api.CheckGroup{},
Objects: []api.CheckObject{},
Error: "the audit was stopped before it finished",
}
}
groups = append(groups, entry.group(sc, objs, spread, keep, pageSize(shown)))
}
return api.CheckReport{
Groups: groups,
Objects: objs.list,
Namespaces: spread.sorted(),
Baseline: keep.takenAt(),
BaselineFrom: keep.takenFrom(),
WasScanned: keep.scannedBefore(),
Scanned: len(sc.subjects),
Error: joined(failure, undiscovered(absent), keep.ruleFailures.message(), strings.Join(importFaults, "; ")),
}
}