Ctrl K
ComunidadGuías…

internal/checks/checks.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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, "; ")),
	}
}