package audit import ( "reflect" "testing" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func annotated(d *appsv1.Deployment, value string) *appsv1.Deployment { d.Annotations = map[string]string{IgnoreChecksAnnotation: value} return d } func TestIgnoreAnnotation_SuppressesNamedCheckOnlyForThatResource(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{ annotated(deploymentInNS("solo-ignored", "prod", 1, secureContainer("app", false)), "singleReplica"), deploymentInNS("solo-flagged", "prod", 1, secureContainer("app", false)), }, } results := RunChecks(input) single := findingNames(results.Findings, "singleReplica") if single["solo-ignored"] { t.Error("singleReplica finding should be suppressed on annotated deployment") } if !single["solo-flagged"] { t.Error("singleReplica finding missing on unannotated deployment") } // Other checks on the annotated resource are unaffected. if !findingNames(results.Findings, "readinessProbeMissing")["solo-ignored"] { t.Error("readinessProbeMissing should still fire on the annotated deployment") } } func TestIgnoreAnnotation_MultipleChecksCommaSeparated(t *testing.T) { d := deploymentInNS("hostnet", "prod", 1, secureContainer("app", false)) d.Spec.Template.Spec.HostNetwork = true input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{annotated(d, "singleReplica, hostNetwork")}, } results := RunChecks(input) for _, id := range []string{"singleReplica", "hostNetwork"} { if findingNames(results.Findings, id)["hostnet"] { t.Errorf("%s finding should be suppressed by comma-separated annotation", id) } } if !findingNames(results.Findings, "readinessProbeMissing")["hostnet"] { t.Error("readinessProbeMissing should still fire on the annotated deployment") } } func TestIgnoreAnnotation_UnknownCheckNameIsHarmless(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{ annotated(deploymentInNS("solo", "prod", 1, secureContainer("app", true)), "notARealCheck"), }, } results := RunChecks(input) if !findingNames(results.Findings, "singleReplica")["solo"] { t.Error("unknown check name in annotation must not suppress anything") } } func TestIgnoreAnnotation_ExcludedFromCounts(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{ annotated(deploymentInNS("solo-ignored", "prod", 1, secureContainer("a", true)), "singleReplica"), deploymentInNS("solo-flagged", "prod", 1, secureContainer("a", true)), deploymentInNS("scaled", "prod", 2, secureContainer("a", true)), }, } results := RunChecks(input) // solo-ignored is excluded from the denominator: 2 evaluated (solo-flagged // failing, scaled passing), not 3. if got := results.CheckCounts["singleReplica"]; got != (CheckCount{Evaluated: 2, Passed: 1}) { t.Errorf("singleReplica counts = %+v, want {Evaluated:2 Passed:1}", got) } if got := results.EvaluatedByNamespace["singleReplica"]["prod"]; got != 2 { t.Errorf("EvaluatedByNamespace[singleReplica][prod] = %d, want 2", got) } } func TestIgnoreAnnotation_MultiContainerDecrementsEvaluatedOnce(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{ // Two containers both missing the readiness probe → two raw // findings for one (resource, check) pair pre-merge. annotated(deploymentInNS("multi", "prod", 2, secureContainer("a", false), secureContainer("b", false)), "readinessProbeMissing"), deploymentInNS("ok", "prod", 2, secureContainer("a", true)), }, } results := RunChecks(input) if findingNames(results.Findings, "readinessProbeMissing")["multi"] { t.Error("readinessProbeMissing should be suppressed on annotated multi-container deployment") } if got := results.CheckCounts["readinessProbeMissing"]; got != (CheckCount{Evaluated: 1, Passed: 1}) { t.Errorf("readinessProbeMissing counts = %+v, want {Evaluated:1 Passed:1}", got) } } func TestIgnoreAnnotation_ReportsIgnoredSubjects(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, Deployments: []*appsv1.Deployment{ // Two containers → two raw findings, but one ignored subject. annotated(deploymentInNS("multi", "prod", 2, secureContainer("a", false), secureContainer("b", false)), "readinessProbeMissing,singleReplica"), annotated(deploymentInNS("solo", "dev", 1, secureContainer("a", true)), "singleReplica"), // Passing subjects produce no finding, so nothing is ignored. annotated(deploymentInNS("ok", "prod", 2, secureContainer("a", true)), "readinessProbeMissing"), }, } results := RunChecks(input) if results.IgnoreChecksAnnotation != IgnoreChecksAnnotation { t.Errorf("IgnoreChecksAnnotation = %q, want %q", results.IgnoreChecksAnnotation, IgnoreChecksAnnotation) } want := map[string]map[string]int{ "readinessProbeMissing": {"prod": 1}, "singleReplica": {"dev": 1}, } if !reflect.DeepEqual(results.AnnotationIgnoredByNamespace, want) { t.Errorf("AnnotationIgnoredByNamespace = %v, want %v", results.AnnotationIgnoredByNamespace, want) } filtered := ApplySettings(results, []string{"dev"}, []string{"readinessProbeMissing"}) if len(filtered.AnnotationIgnoredByNamespace) != 0 { t.Errorf("ignored namespaces and disabled checks must drop their counts, got %v", filtered.AnnotationIgnoredByNamespace) } if filtered.IgnoreChecksAnnotation != IgnoreChecksAnnotation { t.Error("ApplySettings must keep IgnoreChecksAnnotation") } if !reflect.DeepEqual(results.AnnotationIgnoredByNamespace, want) { t.Error("ApplySettings must not mutate the cached scan's map") } } func annotatedUnstructured(u *unstructured.Unstructured, value string) *unstructured.Unstructured { u.SetAnnotations(map[string]string{IgnoreChecksAnnotation: value}) return u } // CRD subjects are keyed by the group audit findings actually carry ("" — CRD // kinds are absent from the builtin table), not by the object's real API group. func TestIgnoreAnnotation_CRDSubject(t *testing.T) { const g = "traefik.io" route := annotatedUnstructured( traefikRoute(g, "IngressRoute", "app", "r1", []map[string]any{{"name": "missing-svc"}}, nil), "traefikRouteMissingService", ) input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, AllServices: []*corev1.Service{}, IngressRoutes: []*unstructured.Unstructured{route}, } results := RunChecks(input) if findingNames(results.Findings, "traefikRouteMissingService")["r1"] { t.Error("traefikRouteMissingService should be suppressed on the annotated IngressRoute") } if got := results.CheckCounts["traefikRouteMissingService"].Evaluated; got != 0 { t.Errorf("Evaluated = %d, want 0 — the ignored route must leave the denominator", got) } } // The opt-out is per-resource: annotating one CRD subject must not silence the // same check on its unannotated siblings, nor steal their evaluated tally. func TestIgnoreAnnotation_CRDSubjectScopedToResource(t *testing.T) { const g = "traefik.io" input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, AllServices: []*corev1.Service{}, IngressRoutes: []*unstructured.Unstructured{ annotatedUnstructured( traefikRoute(g, "IngressRoute", "app", "ignored", []map[string]any{{"name": "missing-a"}}, nil), "traefikRouteMissingService", ), traefikRoute(g, "IngressRoute", "app", "flagged", []map[string]any{{"name": "missing-b"}}, nil), }, } results := RunChecks(input) names := findingNames(results.Findings, "traefikRouteMissingService") if names["ignored"] { t.Error("annotated route should be suppressed") } if !names["flagged"] { t.Error("unannotated sibling route must still be flagged") } if got := results.CheckCounts["traefikRouteMissingService"]; got != (CheckCount{Evaluated: 1, Passed: 0}) { t.Errorf("counts = %+v, want {Evaluated:1 Passed:0}", got) } if got := results.EvaluatedByNamespace["traefikRouteMissingService"]["app"]; got != 1 { t.Errorf("EvaluatedByNamespace[app] = %d, want 1 — the sibling's tally must survive", got) } } // A CRD kind whose name collides with a builtin (CNPG Cluster) must still be // keyed under "" like every other CRD subject. func TestIgnoreAnnotation_CRDSubjectCollidingKind(t *testing.T) { cluster := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "postgresql.cnpg.io/v1", "kind": "Cluster", "metadata": map[string]any{ "name": "db", "namespace": "prod", "annotations": map[string]any{IgnoreChecksAnnotation: "cnpgNoDeclarativeBackup"}, }, }} input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, HorizontalPodAutoscalers: []*autoscalingv2.HorizontalPodAutoscaler{}, CNPGClusters: []*unstructured.Unstructured{cluster}, CNPGScheduledBackupsAuthoritative: true, } results := RunChecks(input) if findingNames(results.Findings, "cnpgNoDeclarativeBackup")["db"] { t.Error("cnpgNoDeclarativeBackup should be suppressed on the annotated CNPG Cluster") } } func TestIgnoreAnnotation_NonWorkloadSubject(t *testing.T) { input := &CheckInput{ ServiceAccounts: []*corev1.ServiceAccount{}, Pods: []*corev1.Pod{}, Services: []*corev1.Service{{ ObjectMeta: metav1.ObjectMeta{ Name: "headless-tool", Namespace: "prod", Annotations: map[string]string{IgnoreChecksAnnotation: "serviceNoMatchingPods"}, }, Spec: corev1.ServiceSpec{Selector: map[string]string{"app": "nothing"}}, }}, } results := RunChecks(input) if findingNames(results.Findings, "serviceNoMatchingPods")["headless-tool"] { t.Error("serviceNoMatchingPods should be suppressed on annotated Service") } }