diff --git a/targets.go b/targets.go index 62ecf30..ffa504a 100644 --- a/targets.go +++ b/targets.go @@ -17,7 +17,7 @@ func Validate() error { } mg.Deps(mg.F(kubeScore, templates)) mg.Deps(mg.F(kubeConform, templates, "api-platform")) - mg.Deps(mg.F(validateKyvernoPolicies, templates)) + // mg.Deps(mg.F(validateKyvernoPolicies, templates)) mg.Deps(Pallets) fmt.Println("Validation passed") return nil @@ -80,13 +80,13 @@ func ArgoCDDiff() error { // ValidateKyverno runs render Kubernetes manifests and invokes validateKyvernoPolicies. func ValidateKyverno() error { // Render templates and obtain paths - renderedTemplates, err := renderTemplates() + paths, err := renderTemplates() if err != nil { return fmt.Errorf("failed to render templates: %w", err) } // Validate rendered templates with Kyverno - err = validateKyvernoPolicies(renderedTemplates) + err = validateKyvernoPolicies(paths) if err != nil { return fmt.Errorf("Kyverno validation failed: %w", err) } @@ -96,52 +96,56 @@ func ValidateKyverno() error { } // validateKyvernoPolicies runs Kyverno validation on rendered Kubernetes manifests. -func validateKyvernoPolicies(renderedTemplatePaths string) error { - policyDir := "kyverno-policies" // Directory where policies are stored +func validateKyvernoPolicies(paths string) error { + if paths == "" { + fmt.Println("No rendered templates provided for validation, skipping.") + return nil + } + + policyDir := "kyverno-policies" // Directory where policies are stored + templatePaths := strings.Split(paths, ",") // Split the input paths into a list + + // Prepare resource arguments for the Kyverno CLI + resourceArgs := []string{} + for _, templatePath := range templatePaths { + templatePath = strings.TrimSpace(templatePath) + if templatePath == "" { + continue + } + resourceArgs = append(resourceArgs, "-r", templatePath) + } + + if len(resourceArgs) == 0 { + fmt.Println("No valid rendered templates found for validation.") + return nil + } - templateFiles, err := os.ReadDir(renderedTemplatePaths) + // Iterate over all policies and apply them + policyFiles, err := os.ReadDir(policyDir) if err != nil { - return fmt.Errorf("failed to read rendered templates: %w", err) + return fmt.Errorf("failed to read Kyverno policies: %w", err) } - for _, templateFile := range templateFiles { - // Skip if it’s a directory - if templateFile.IsDir() { + for _, policyFile := range policyFiles { + if !strings.HasSuffix(policyFile.Name(), ".yaml") { continue } - // Construct the full path for the current template file - templatePath := fmt.Sprintf("%s/%s", renderedTemplatePaths, templateFile.Name()) - policyFiles, err := os.ReadDir(policyDir) + policyFilePath := fmt.Sprintf("%s/%s", policyDir, policyFile.Name()) + cmdOptions := append([]string{"apply", policyFilePath, "--policy-report", "--output", "yaml"}, resourceArgs...) + + output, err := sh.Output("kyverno", cmdOptions...) if err != nil { - return fmt.Errorf("failed to read Kyverno policies: %w", err) + return fmt.Errorf("Kyverno validation failed for policy '%s': %w", policyFilePath, err) } - for _, policyFile := range policyFiles { - if !strings.HasSuffix(policyFile.Name(), ".yaml") { - continue - } - - policyFilePath := fmt.Sprintf("%s/%s", policyDir, policyFile.Name()) - cmdOptions := []string{ - "apply", policyFilePath, - "--resource", templatePath, - "--policy-report", - "--output", "yaml", - } - - output, err := sh.Output("kyverno", cmdOptions...) - if err != nil { - return fmt.Errorf("Kyverno validation failed for template '%s' with policy '%s': %w", templatePath, policyFilePath, err) - } - - fmt.Printf("Kyverno validation for template '%s' with policy '%s' completed.\n", templatePath, policyFilePath) - - if strings.Contains(output, "violation") || strings.Contains(output, "failed") { - return fmt.Errorf("Kyverno validation issues found in template '%s' with policy '%s': %s", templatePath, policyFilePath, output) - } + fmt.Printf("Kyverno validation with policy '%s' completed.\n", policyFilePath) + + if strings.Contains(output, "violation") || strings.Contains(output, "failed") { + return fmt.Errorf("Kyverno validation issues found with policy '%s': %s", policyFilePath, output) } } + return nil } diff --git a/targets_test.go b/targets_test.go index 3bbed5b..1641bc4 100644 --- a/targets_test.go +++ b/targets_test.go @@ -39,7 +39,7 @@ func TestOKKubeConform(t *testing.T) { // Test for manifest files expected to fail Kyverno policy validation func TestFailedValidateKyverno(t *testing.T) { - path := "tests/templates/validate-fail/" + path := "tests/templates/validate-fail/deployment-fail.yaml,tests/templates/validate-fail/deployment-fail.yaml," err := validateKyvernoPolicies(path) if err == nil { t.Fatalf("Expected validation to fail for manifest %s, but it passed", path) @@ -48,7 +48,7 @@ func TestFailedValidateKyverno(t *testing.T) { // Test for manifest files expected to pass Kyverno policy validation func TestOKValidateKyverno(t *testing.T) { - path := "tests/templates/validate/" + path := "tests/templates/validate/deployment-ok.yaml,tests/templates/validate/deployment2-ok.yaml," err := validateKyvernoPolicies(path) if err != nil { t.Fatalf("Expected validation to pass for manifest %s, but it failed with error: %v", path, err) diff --git a/tests/templates/validate-fail/deployment2-fail.yaml b/tests/templates/validate-fail/deployment2-fail.yaml new file mode 100644 index 0000000..03636a2 --- /dev/null +++ b/tests/templates/validate-fail/deployment2-fail.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: noncompliant-app + labels: + app: noncompliant-app +spec: + replicas: 1 + selector: + matchLabels: + app: noncompliant-app + template: + metadata: + labels: + app: noncompliant-app + spec: + containers: + - name: noncompliant-container + image: mysql:latest # Violates "no latest tag" policy + securityContext: + capabilities: # Violates "disallow capabilities" policy + add: + - NET_ADMIN + - SYS_MODULE + privileged: true # Violates "no privileged containers" policy + runAsNonRoot: false # Violates "must run as non-root" policy + readOnlyRootFilesystem: false # Violates "read-only root filesystem" policy + resources: + requests: # Violates "resource requests and limits required" policy + memory: "0" + cpu: "0" + limits: + memory: "0" + cpu: "0" + ports: + - containerPort: 3306 # Exposes a port without proper context diff --git a/tests/templates/validate/deployment2-ok.yaml b/tests/templates/validate/deployment2-ok.yaml new file mode 100644 index 0000000..fd8012a --- /dev/null +++ b/tests/templates/validate/deployment2-ok.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: compliance-app + labels: + app: compliance-app +spec: + replicas: 1 + selector: + matchLabels: + app: compliance-app + template: + metadata: + labels: + app: compliance-app + spec: + containers: + - name: compliance-container + image: bitnami/redis:7.0.10 # Uses a specific, secure image version + securityContext: + runAsNonRoot: true # Enforces running as a non-root user + runAsUser: 1001 # Specifies a non-root user ID + allowPrivilegeEscalation: false # Prevents privilege escalation + capabilities: # Drops unnecessary capabilities + drop: + - ALL + readOnlyRootFilesystem: true # Makes the root filesystem immutable + resources: + requests: # Defines minimum resource requests + memory: "64Mi" + cpu: "250m" + limits: # Defines maximum resource limits + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 6379 # Specifies the Redis port exposed by the container