Ctrl K
ComunidadGuías…

pkg/policyreports/source_test.go

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

Archivo original ↗Preguntar a KiBot
package policyreports

import (
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func TestBuildIndex_ExtractsSource(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{
			"policy":    "require-run-as-nonroot",
			"rule":      "check-containers",
			"result":    "fail",
			"source":    "KyvernoValidatingPolicy",
			"message":   "runAsNonRoot must be true",
			"resources": []any{resourceRefWithGroup("apps/v1", "Deployment", "prod", "api")},
		},
	})

	got := BuildIndex([]*unstructured.Unstructured{r}).FindingsFor("apps", "Deployment", "prod", "api")
	if len(got) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(got))
	}
	if got[0].Source != "KyvernoValidatingPolicy" {
		t.Errorf("Source = %q, want %q", got[0].Source, "KyvernoValidatingPolicy")
	}
	if got[0].Engine() != EngineKyverno {
		t.Errorf("Engine() = %q, want %q", got[0].Engine(), EngineKyverno)
	}
}

// A report entry with no `source` must not fabricate one. Older engines
// omit the field entirely and the resulting finding has to stay
// attributable-as-unknown rather than defaulting to Kyverno just because
// this index started life as a Kyverno-only feature.
func TestBuildIndex_MissingSourceStaysUnknown(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{
			"policy":    "legacy-policy",
			"rule":      "some-rule",
			"result":    "fail",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
	})

	got := BuildIndex([]*unstructured.Unstructured{r}).FindingsFor("", "Pod", "prod", "api-1")
	if len(got) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(got))
	}
	if got[0].Source != "" {
		t.Errorf("Source = %q, want empty", got[0].Source)
	}
	if got[0].Engine() != EngineUnknown {
		t.Errorf("Engine() = %q, want %q", got[0].Engine(), EngineUnknown)
	}
}

// The multi-engine case the taxonomy exists for: one subject carrying
// findings from legacy Kyverno, modern Kyverno, VAP and Trivy at once.
// Filtering by EngineKyverno must return BOTH Kyverno results and nothing
// else — the exact behaviour a raw-source match would get wrong.
func TestFindingsForEngine_CollapsesProducerTypesAndExcludesOthers(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{
			"policy": "legacy-check", "result": "fail", "source": "kyverno",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
		{
			"policy": "modern-check", "result": "fail", "source": "KyvernoValidatingPolicy",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
		{
			"policy": "vap-check", "result": "fail", "source": "ValidatingAdmissionPolicy",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
		{
			"policy": "cve-check", "result": "fail", "source": "Trivy Vulnerability",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
	})

	idx := BuildIndex([]*unstructured.Unstructured{r})

	kyverno := idx.FindingsForEngine("", "Pod", "prod", "api-1", EngineKyverno)
	if len(kyverno) != 2 {
		t.Fatalf("EngineKyverno returned %d findings, want 2 (legacy + modern)", len(kyverno))
	}
	for _, f := range kyverno {
		if f.Engine() != EngineKyverno {
			t.Errorf("policy %q leaked into the kyverno bucket with engine %q", f.Policy, f.Engine())
		}
	}

	if got := idx.FindingsForEngine("", "Pod", "prod", "api-1", EngineVAP); len(got) != 1 {
		t.Errorf("EngineVAP returned %d findings, want 1", len(got))
	}
	if got := idx.FindingsForEngine("", "Pod", "prod", "api-1", EngineTrivy); len(got) != 1 {
		t.Errorf("EngineTrivy returned %d findings, want 1", len(got))
	}
	if got := idx.FindingsForEngine("", "Pod", "prod", "api-1", EngineFalco); got != nil {
		t.Errorf("EngineFalco returned %d findings, want nil", len(got))
	}

	// The unfiltered accessor keeps returning everything.
	if got := idx.FindingsFor("", "Pod", "prod", "api-1"); len(got) != 4 {
		t.Errorf("FindingsFor returned %d findings, want 4", len(got))
	}
}

func TestFindingsForEngine_NilSafe(t *testing.T) {
	var idx *Index
	if got := idx.FindingsForEngine("", "Pod", "prod", "api-1", EngineKyverno); got != nil {
		t.Errorf("nil index returned %d findings, want nil", len(got))
	}
}

// A cluster mid-migration can serve both report families with overlapping
// content, and the selection layer watches both when both hold data. The same
// (policy, rule, result, message, source) on the same subject carries no extra
// information whichever report it arrived in, and counting it twice would
// inflate every violation total the UI and agents show.
func TestBuildIndex_DeduplicatesIdenticalFindingsAcrossReports(t *testing.T) {
	now := time.Now()
	result := map[string]any{
		"policy": "require-labels", "rule": "check", "result": "fail",
		"source": "KyvernoValidatingPolicy", "message": "needs a team label",
		"resources": []any{resourceRef("Pod", "prod", "api-1")},
	}
	// Same finding, carried by two reports from two API families.
	wg := makeReport(t, "PolicyReport", "prod", "wg-1", nil, now, []map[string]any{result})
	or := makeReport(t, "Report", "prod", "or-1", nil, now, []map[string]any{result})

	got := BuildIndex([]*unstructured.Unstructured{wg, or}).FindingsFor("", "Pod", "prod", "api-1")
	if len(got) != 1 {
		t.Fatalf("expected the duplicate to be collapsed to 1 finding, got %d", len(got))
	}
}

// Dedup must key on the whole finding, not just the policy — two different
// rules of the same policy failing on one subject are two real findings.
func TestBuildIndex_KeepsDistinctFindingsFromTheSamePolicy(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{
			"policy": "baseline", "rule": "no-host-network", "result": "fail", "source": "kyverno",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
		{
			"policy": "baseline", "rule": "no-privileged", "result": "fail", "source": "kyverno",
			"resources": []any{resourceRef("Pod", "prod", "api-1")},
		},
	})

	got := BuildIndex([]*unstructured.Unstructured{r}).FindingsFor("", "Pod", "prod", "api-1")
	if len(got) != 2 {
		t.Fatalf("expected 2 distinct findings, got %d", len(got))
	}
}

// falcosidekick hardcodes the producer string as the literal "Falco" and puts
// its own detection source ("syscall"/"k8s_audit") in the POLICY field. Pinned
// because the taxonomy has to attribute on Source, never on Policy.
func TestEngineForSource_FalcosidekickLiteral(t *testing.T) {
	f := Finding{Policy: "syscall", Rule: "Terminal shell in container", Source: "Falco"}
	if f.Engine() != EngineFalco {
		t.Errorf("Engine() = %q, want %q", f.Engine(), EngineFalco)
	}
}

// Bugbot #3 (MEDIUM): the Kyverno-labelled rollup must not absorb other
// engines. The index is shared — Trivy, Falco adapters and VAP evaluation all
// write into the same report families — so an unfiltered read presented as
// "kyverno" over-counts as soon as a second engine is installed.
func TestFindingsForAnyEngine_KyvernoAttributionSet(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{"policy": "modern", "result": "fail", "source": "KyvernoValidatingPolicy",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
		{"policy": "legacy", "result": "fail", "source": "kyverno",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
		// No source at all — older Kyverno. Must still count, or the rollup
		// empties out on exactly the legacy clusters we still serve.
		{"policy": "unattributed", "result": "fail",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
		{"policy": "vap", "result": "fail", "source": "ValidatingAdmissionPolicy",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
		{"policy": "cve", "result": "fail", "source": "Trivy Vulnerability",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
		{"policy": "syscall", "result": "fail", "source": "Falco",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
	})

	got := BuildIndex([]*unstructured.Unstructured{r}).
		FindingsForAnyEngine("", "Pod", "prod", "api-1", EnginesAttributableToKyverno...)

	if len(got) != 3 {
		t.Fatalf("got %d findings, want 3 (modern + legacy + unattributed)", len(got))
	}
	for _, f := range got {
		switch f.Policy {
		case "modern", "legacy", "unattributed":
		default:
			t.Errorf("%q (source %q) leaked into the Kyverno rollup", f.Policy, f.Source)
		}
	}
}

// An empty filter must mean "nothing matches", never "everything" — a caller
// that accidentally passes no engines should get an obviously empty result
// rather than silently unfiltered data.
func TestFindingsForAnyEngine_EmptyFilterReturnsNothing(t *testing.T) {
	now := time.Now()
	r := makeReport(t, "PolicyReport", "prod", "pr-1", nil, now, []map[string]any{
		{"policy": "a", "result": "fail", "source": "kyverno",
			"resources": []any{resourceRef("Pod", "prod", "api-1")}},
	})

	if got := BuildIndex([]*unstructured.Unstructured{r}).FindingsForAnyEngine("", "Pod", "prod", "api-1"); got != nil {
		t.Errorf("empty engine filter returned %d findings, want nil", len(got))
	}
}