From 2fb2314ad942bdd89a89c0d82ec10381316c9783 Mon Sep 17 00:00:00 2001 From: taimurhafeez Date: Tue, 24 Mar 2026 13:01:19 +0000 Subject: [PATCH 1/5] Add namespace exemption test for resource limit checks --- e2e_test.go | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) diff --git a/e2e_test.go b/e2e_test.go index c19f9f75..11a842bd 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -497,3 +497,119 @@ func TestProfileRemediations(t *testing.T) { t.Logf("Warning: Failed to wait for scan cleanup for binding %s: %s", bindingName, err) } } + +// TestNamespaceExemptionVariables tests the namespace exemption logic for +// resource limit checks (test case 76797). This test validates that: +// 1. Workloads without resource limits in exempted namespaces pass the check +// 2. The exemption variables work correctly for DaemonSet, Deployment, and StatefulSet +func TestNamespaceExemptionVariables(t *testing.T) { + // Skip if test type doesn't include platform tests + if tc.TestType != "platform" && tc.TestType != "all" { + t.Skipf("Skipping namespace exemption test: -test-type is %s", tc.TestType) + } + + c, err := helpers.GenerateKubeConfig() + if err != nil { + t.Fatalf("Failed to generate kube config: %s", err) + } + + // Test namespace names + testNamespaces := []string{ + "ns-76797-test-1", + "ns-76797-test-2", + } + + // Create test namespaces + for _, ns := range testNamespaces { + err = createNamespace(c, ns) + if err != nil { + t.Fatalf("Failed to create test namespace %s: %s", ns, err) + } + t.Logf("Created test namespace: %s", ns) + } + + // Cleanup namespaces at the end + defer func() { + for _, ns := range testNamespaces { + deleteNamespace(c, ns) + } + }() + + // Create workloads without resource limits in test namespaces + err = createTestWorkloadsWithoutLimits(c, testNamespaces[0]) + if err != nil { + t.Fatalf("Failed to create test workloads: %s", err) + } + t.Logf("Created test workloads without resource limits in %s", testNamespaces[0]) + + // Wait for workloads to be created + time.Sleep(5 * time.Second) + + // Build regex pattern to exempt test namespaces + // Pattern matches both test namespaces + exemptionPattern := "^ns-76797-test-.*$" + + // Create TailoredProfile with namespace exemption variables + tailoredProfileName := "ns-exemption-test-profile" + err = createTailoredProfileWithExemptions(tc, c, tailoredProfileName, exemptionPattern) + if err != nil { + t.Fatalf("Failed to create tailored profile with exemptions: %s", err) + } + t.Logf("Created TailoredProfile: %s with exemption pattern: %s", tailoredProfileName, exemptionPattern) + + // Create scan binding for the tailored profile + bindingName := "ns-exemption-scan-binding" + err = helpers.CreateScanBinding(c, tc, bindingName, tailoredProfileName, "TailoredProfile", "default") + if err != nil { + t.Fatalf("Failed to create scan binding: %s", err) + } + t.Logf("Created ScanSettingBinding: %s", bindingName) + + // Wait for compliance suite to complete + err = helpers.WaitForComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to wait for compliance suite: %s", err) + } + + // Get scan results + results, err := helpers.CreateResultMap(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to create result map: %s", err) + } + + // Verify that resource limit rules PASS because namespaces are exempted + expectedRules := map[string]string{ + "ocp4-resource-requests-limits-in-daemonset": "PASS", + "ocp4-resource-requests-limits-in-deployment": "PASS", + "ocp4-resource-requests-limits-in-statefulset": "PASS", + } + + var failures []string + for ruleName, expectedResult := range expectedRules { + // Find the actual result - the result name might include scan name prefix + actualResult := findRuleResult(results, ruleName) + if actualResult == "" { + failures = append(failures, fmt.Sprintf("Rule %s not found in scan results", ruleName)) + continue + } + + if actualResult != expectedResult { + failures = append(failures, + fmt.Sprintf("Rule %s: expected %s, got %s", ruleName, expectedResult, actualResult)) + } else { + t.Logf("✓ Rule %s: %s (namespace exemption working correctly)", ruleName, actualResult) + } + } + + // Save results for debugging + err = helpers.SaveResultAsYAML(tc, results, "namespace-exemption-test-results.yaml") + if err != nil { + t.Logf("Warning: Failed to save test results: %s", err) + } + + if len(failures) > 0 { + t.Fatalf("Namespace exemption test failed:\n%v", failures) + } + + t.Log("Namespace exemption test passed successfully - all exempted workloads passed the resource limit checks") +} diff --git a/helpers.go b/helpers.go index ce42406d..e44dd14e 100644 --- a/helpers.go +++ b/helpers.go @@ -1,5 +1,17 @@ package ocp4e2e +import ( + "context" + "strings" + + cmpv1alpha1 "github.com/ComplianceAsCode/compliance-operator/pkg/apis/compliance/v1alpha1" + "github.com/ComplianceAsCode/ocp4e2e/config" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + dynclient "sigs.k8s.io/controller-runtime/pkg/client" +) + // RuleTest is the definition of the structure rule-specific e2e tests should have. type RuleTest struct { DefaultResult interface{} `yaml:"default_result"` @@ -13,3 +25,114 @@ type RuleTestResults struct { func init() { } + +// createTailoredProfileWithExemptions creates a TailoredProfile for namespace exemption testing. +func createTailoredProfileWithExemptions(tc *config.TestConfig, c dynclient.Client, name, exemptionPattern string) error { + tp := &cmpv1alpha1.TailoredProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, Namespace: tc.OperatorNamespace.Namespace, + Annotations: map[string]string{"compliance.openshift.io/product-type": "Platform"}, + }, + Spec: cmpv1alpha1.TailoredProfileSpec{ + Title: "Namespace Exemption Test Profile", + Description: "Test profile for validating namespace exemption variables", + EnableRules: []cmpv1alpha1.RuleReferenceSpec{ + {Name: "ocp4-resource-requests-limits-in-daemonset"}, + {Name: "ocp4-resource-requests-limits-in-deployment"}, + {Name: "ocp4-resource-requests-limits-in-statefulset"}, + }, + SetValues: []cmpv1alpha1.VariableValueSpec{ + {Name: "ocp4-var-daemonset-limit-namespaces-exempt-regex", Value: exemptionPattern}, + {Name: "ocp4-var-deployment-limit-namespaces-exempt-regex", Value: exemptionPattern}, + {Name: "ocp4-var-statefulset-limit-namespaces-exempt-regex", Value: exemptionPattern}, + }, + }, + } + return c.Create(context.TODO(), tp) +} + +// createTestWorkloadsWithoutLimits creates test workloads without resource limits. +func createTestWorkloadsWithoutLimits(c dynclient.Client, namespace string) error { + ctx := context.TODO() + + workloads := []dynclient.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test-deployment-no-limits", Namespace: namespace}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}}, + }, + }, + }, + &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Name: "test-daemonset-no-limits", Namespace: namespace}, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}, + Tolerations: []corev1.Toleration{{Operator: corev1.TolerationOpExists}}, + }, + }, + }, + }, + &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "test-statefulset-no-limits", Namespace: namespace}, + Spec: appsv1.StatefulSetSpec{ + Replicas: int32Ptr(1), ServiceName: "test", + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}}, + }, + }, + }, + } + + for _, w := range workloads { + if err := c.Create(ctx, w); err != nil { + return err + } + } + return nil +} + +// createNamespace creates a namespace. +func createNamespace(c dynclient.Client, name string) error { + return c.Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}) +} + +// deleteNamespace deletes a namespace. +func deleteNamespace(c dynclient.Client, name string) error { + c.Delete(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}) + return nil +} + +// findRuleResult searches for a rule result by partial name match. +func findRuleResult(results map[string]string, ruleName string) string { + if result, exists := results[ruleName]; exists { + return result + } + for resultName, resultValue := range results { + if strings.Contains(resultName, ruleName) { + return resultValue + } + } + return "" +} + +// int32Ptr returns a pointer to an int32 value. +func int32Ptr(i int32) *int32 { return &i } From 66d0d0229d41d619ab5d3b03777c6e34302cb123 Mon Sep 17 00:00:00 2001 From: taimurhafeez Date: Tue, 24 Mar 2026 13:02:28 +0000 Subject: [PATCH 2/5] removed downstream test ID --- e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e_test.go b/e2e_test.go index 11a842bd..6208218d 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -499,7 +499,7 @@ func TestProfileRemediations(t *testing.T) { } // TestNamespaceExemptionVariables tests the namespace exemption logic for -// resource limit checks (test case 76797). This test validates that: +// resource limit checks. This test validates that: // 1. Workloads without resource limits in exempted namespaces pass the check // 2. The exemption variables work correctly for DaemonSet, Deployment, and StatefulSet func TestNamespaceExemptionVariables(t *testing.T) { From 0051737422bf7ac1f114e3d4046dbe9851794b47 Mon Sep 17 00:00:00 2001 From: taimurhafeez Date: Tue, 24 Mar 2026 16:52:08 +0000 Subject: [PATCH 3/5] Fix rule name matching in namespace exemption test --- e2e_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e_test.go b/e2e_test.go index 6208218d..57aef1c2 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -579,9 +579,9 @@ func TestNamespaceExemptionVariables(t *testing.T) { // Verify that resource limit rules PASS because namespaces are exempted expectedRules := map[string]string{ - "ocp4-resource-requests-limits-in-daemonset": "PASS", - "ocp4-resource-requests-limits-in-deployment": "PASS", - "ocp4-resource-requests-limits-in-statefulset": "PASS", + "resource-requests-limits-in-daemonset": "PASS", + "resource-requests-limits-in-deployment": "PASS", + "resource-requests-limits-in-statefulset": "PASS", } var failures []string From 9a54826b8a8efd5f30ee8b45dd191297b2888578 Mon Sep 17 00:00:00 2001 From: taimurhafeez Date: Tue, 24 Mar 2026 16:57:03 +0000 Subject: [PATCH 4/5] removed icons from printed log --- e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e_test.go b/e2e_test.go index 57aef1c2..90bbbdbc 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -597,7 +597,7 @@ func TestNamespaceExemptionVariables(t *testing.T) { failures = append(failures, fmt.Sprintf("Rule %s: expected %s, got %s", ruleName, expectedResult, actualResult)) } else { - t.Logf("✓ Rule %s: %s (namespace exemption working correctly)", ruleName, actualResult) + t.Logf("Rule %s: %s (namespace exemption working correctly)", ruleName, actualResult) } } From 3216a1433be83722ce00d23fa5afd0b2348b1baa Mon Sep 17 00:00:00 2001 From: taimurhafeez Date: Thu, 26 Mar 2026 15:44:03 +0000 Subject: [PATCH 5/5] Added test for incorrect Kubeletconfig works using remediations --- e2e_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 103 ++++++++++++++++++++++++ 2 files changed, 324 insertions(+) diff --git a/e2e_test.go b/e2e_test.go index 90bbbdbc..164b3946 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -6,6 +6,7 @@ import ( "log" "os" "path" + "strings" "testing" "time" @@ -613,3 +614,223 @@ func TestNamespaceExemptionVariables(t *testing.T) { t.Log("Namespace exemption test passed successfully - all exempted workloads passed the resource limit checks") } + +// TestKubeletConfigAutoRemediation tests that auto-remediation properly updates +// an existing KubeletConfig object with insecure tlsCipherSuites configuration. +// This test validates: +// 1. A KubeletConfig with insecure cipher suites is created and applied to nodes +// 2. Auto-remediation updates the KubeletConfig with secure cipher suites +// 3. The kubelet-configure-tls-cipher-suites rule passes after remediation +func TestKubeletConfigAutoRemediation(t *testing.T) { + // Skip if test type doesn't include node tests + if tc.TestType != "node" && tc.TestType != "all" { + t.Skipf("Skipping KubeletConfig auto-remediation test: -test-type is %s", tc.TestType) + } + + c, err := helpers.GenerateKubeConfig() + if err != nil { + t.Fatalf("Failed to generate kube config: %s", err) + } + + // Test configuration + kubeletConfigName := "test-kubelet-tls-cipher" + machineConfigPoolName := "worker" + bindingName := "kubeletconfig-autorem-test" + + // Expected cipher suite value after remediation + expectedCipher := "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" + + // Cleanup function to remove test resources + defer func() { + t.Log("Cleaning up test resources") + + // Delete scan binding + err := helpers.DeleteScanBinding(tc, c, bindingName) + if err != nil { + t.Logf("Warning: Failed to delete scan binding: %s", err) + } + + // Wait for scan cleanup + err = helpers.WaitForScanCleanup(tc, c, bindingName) + if err != nil { + t.Logf("Warning: Failed to wait for scan cleanup: %s", err) + } + + // Delete KubeletConfig + deleteKubeletConfig(c, kubeletConfigName) + + // Wait for MachineConfigPool to stabilize + time.Sleep(30 * time.Second) + err = helpers.WaitForMachineConfigPoolsUpdated(tc, c) + if err != nil { + t.Logf("Warning: Failed to wait for MachineConfigPools after cleanup: %s", err) + } + }() + + // Step 1: Create a KubeletConfig with insecure tlsCipherSuites + t.Logf("Creating KubeletConfig '%s' with insecure tlsCipherSuites", kubeletConfigName) + err = createKubeletConfigWithEmptyCiphers(c, kubeletConfigName, machineConfigPoolName) + if err != nil { + t.Fatalf("Failed to create KubeletConfig: %s", err) + } + + // Wait for KubeletConfig to be applied + time.Sleep(10 * time.Second) + + // Verify initial KubeletConfig state (insecure ciphers) + initialCiphers, err := getKubeletConfigCiphers(c, kubeletConfigName) + if err != nil { + t.Fatalf("Failed to get initial KubeletConfig ciphers: %s", err) + } + + if len(initialCiphers) == 0 { + t.Fatal("Expected insecure tlsCipherSuites but found empty list") + } else { + t.Logf("Verified: KubeletConfig has insecure tlsCipherSuites initially: %v", initialCiphers) + } + + // Wait for MachineConfigPool to apply the insecure KubeletConfig to nodes + t.Log("Waiting for MachineConfigPool to apply insecure KubeletConfig to nodes (this may take 10-20 minutes)") + err = helpers.WaitForMachineConfigPoolsUpdated(tc, c) + if err != nil { + t.Fatalf("Failed to wait for MachineConfigPools to apply KubeletConfig: %s", err) + } + t.Log("MachineConfigPool updated - nodes now have insecure tlsCipherSuites, scan should detect FAIL") + + // Step 2: Create a tailored profile with CIS rules and auto-apply enabled + t.Log("Creating tailored profile for CIS with kubelet TLS cipher rules") + tailoredProfileName := "cis-kubelet-autorem-test" + err = createCISKubeletTailoredProfile(tc, c, tailoredProfileName) + if err != nil { + t.Fatalf("Failed to create tailored profile: %s", err) + } + + // Step 3: Create scan binding with auto-apply remediations + t.Log("Creating scan binding with auto-apply remediations enabled") + err = helpers.CreateScanBinding(c, tc, bindingName, tailoredProfileName, "TailoredProfile", tc.E2eSettings) + if err != nil { + t.Fatalf("Failed to create scan binding: %s", err) + } + + // Step 4: Wait for compliance suite to complete + t.Log("Waiting for compliance suite to complete first scan") + err = helpers.WaitForComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to wait for compliance suite: %s", err) + } + + // Get initial scan results + initialResults, err := helpers.CreateResultMap(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to create initial result map: %s", err) + } + + // Step 5: Wait for auto-remediations to be applied + t.Log("Waiting for auto-remediations to be applied") + time.Sleep(30 * time.Second) + + err = helpers.WaitForRemediationsToBeApplied(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed waiting for remediations to be applied: %s", err) + } + + // Step 6: Wait for MachineConfigPool to update after remediation + t.Log("Waiting for MachineConfigPool to update after remediation") + err = helpers.WaitForMachineConfigPoolsUpdated(tc, c) + if err != nil { + t.Fatalf("Failed to wait for MachineConfigPools: %s", err) + } + + // Step 7: Verify KubeletConfig was updated with correct ciphers + t.Log("Verifying KubeletConfig was updated with correct TLS cipher suites") + updatedCiphers, err := getKubeletConfigCiphers(c, kubeletConfigName) + if err != nil { + t.Fatalf("Failed to get updated KubeletConfig ciphers: %s", err) + } + + if len(updatedCiphers) == 0 { + t.Fatal("KubeletConfig tlsCipherSuites is still empty after auto-remediation") + } + + // Check that insecure ciphers were replaced + hasInsecureCipher := false + for _, cipher := range updatedCiphers { + if cipher == "TLS_RSA_WITH_AES_128_CBC_SHA" || cipher == "TLS_RSA_WITH_AES_256_CBC_SHA" { + hasInsecureCipher = true + break + } + } + if hasInsecureCipher { + t.Fatalf("KubeletConfig still contains insecure ciphers after remediation: %v", updatedCiphers) + } + + // Check if expected cipher is present + cipherFound := false + for _, cipher := range updatedCiphers { + if cipher == expectedCipher { + cipherFound = true + break + } + } + + if !cipherFound { + t.Fatalf("Expected cipher '%s' not found in KubeletConfig. Found: %v", expectedCipher, updatedCiphers) + } + t.Logf("SUCCESS: KubeletConfig updated with correct ciphers: %v", updatedCiphers) + + // Step 8: Trigger rescan to verify rules pass + t.Log("Triggering rescan to verify rules pass after remediation") + err = helpers.RescanComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to trigger rescan: %s", err) + } + + err = helpers.WaitForComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to wait for rescan: %s", err) + } + + // Step 9: Verify final scan results + finalResults, err := helpers.CreateResultMap(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to create final result map: %s", err) + } + + // Save results for debugging + err = helpers.SaveResultAsYAML(tc, initialResults, "kubeletconfig-autorem-initial-results.yaml") + if err != nil { + t.Logf("Warning: Failed to save initial results: %s", err) + } + + err = helpers.SaveResultAsYAML(tc, finalResults, "kubeletconfig-autorem-final-results.yaml") + if err != nil { + t.Logf("Warning: Failed to save final results: %s", err) + } + + // Verify critical rules pass + criticalRules := map[string]string{ + "kubelet-configure-tls-cipher-suites": "PASS", + } + + var failures []string + for ruleName, expectedResult := range criticalRules { + actualResult := findRuleResult(finalResults, ruleName) + if actualResult == "" { + failures = append(failures, fmt.Sprintf("Rule %s not found in scan results", ruleName)) + continue + } + + if actualResult != expectedResult { + failures = append(failures, + fmt.Sprintf("Rule %s: expected %s, got %s", ruleName, expectedResult, actualResult)) + } else { + t.Logf("✓ Rule %s: %s", ruleName, actualResult) + } + } + + if len(failures) > 0 { + t.Fatalf("KubeletConfig auto-remediation test failed:\n%s", strings.Join(failures, "\n")) + } + + t.Log("✓ KubeletConfig auto-remediation test passed successfully") +} diff --git a/helpers.go b/helpers.go index e44dd14e..3fe97c91 100644 --- a/helpers.go +++ b/helpers.go @@ -2,6 +2,8 @@ package ocp4e2e import ( "context" + "fmt" + "log" "strings" cmpv1alpha1 "github.com/ComplianceAsCode/compliance-operator/pkg/apis/compliance/v1alpha1" @@ -9,6 +11,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" dynclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -136,3 +139,103 @@ func findRuleResult(results map[string]string, ruleName string) string { // int32Ptr returns a pointer to an int32 value. func int32Ptr(i int32) *int32 { return &i } + +// createKubeletConfigWithEmptyCiphers creates a KubeletConfig with insecure tlsCipherSuites +func createKubeletConfigWithEmptyCiphers(c dynclient.Client, name, poolName string) error { + kubeletConfig := map[string]interface{}{ + "apiVersion": "machineconfiguration.openshift.io/v1", + "kind": "KubeletConfig", + "metadata": map[string]interface{}{ + "name": name, + }, + "spec": map[string]interface{}{ + "machineConfigPoolSelector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "pools.operator.machineconfiguration.openshift.io/" + poolName: "", + }, + }, + "kubeletConfig": map[string]interface{}{ + "tlsCipherSuites": []string{ + // Set weak/insecure ciphers that should fail the check + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + }, + }, + }, + } + + obj := &unstructured.Unstructured{Object: kubeletConfig} + return c.Create(context.TODO(), obj) +} + +// getKubeletConfigCiphers retrieves the tlsCipherSuites from a KubeletConfig +func getKubeletConfigCiphers(c dynclient.Client, name string) ([]string, error) { + kubeletConfig := &unstructured.Unstructured{} + kubeletConfig.SetAPIVersion("machineconfiguration.openshift.io/v1") + kubeletConfig.SetKind("KubeletConfig") + kubeletConfig.SetName(name) + + err := c.Get(context.TODO(), dynclient.ObjectKey{Name: name}, kubeletConfig) + if err != nil { + return nil, err + } + + spec, found, err := unstructured.NestedMap(kubeletConfig.Object, "spec", "kubeletConfig") + if err != nil || !found { + return []string{}, nil + } + + ciphersInterface, found := spec["tlsCipherSuites"] + if !found { + return []string{}, nil + } + + ciphersList, ok := ciphersInterface.([]interface{}) + if !ok { + return []string{}, fmt.Errorf("tlsCipherSuites is not a list") + } + + ciphers := make([]string, 0, len(ciphersList)) + for _, c := range ciphersList { + if cipherStr, ok := c.(string); ok { + ciphers = append(ciphers, cipherStr) + } + } + + return ciphers, nil +} + +// deleteKubeletConfig deletes a KubeletConfig object +func deleteKubeletConfig(c dynclient.Client, name string) { + kubeletConfig := &unstructured.Unstructured{} + kubeletConfig.SetAPIVersion("machineconfiguration.openshift.io/v1") + kubeletConfig.SetKind("KubeletConfig") + kubeletConfig.SetName(name) + + err := c.Delete(context.TODO(), kubeletConfig) + if err != nil { + log.Printf("Warning: Failed to delete KubeletConfig %s: %s", name, err) + } +} + +// createCISKubeletTailoredProfile creates a tailored profile for testing CIS kubelet rules +func createCISKubeletTailoredProfile(tc *config.TestConfig, c dynclient.Client, name string) error { + tp := &cmpv1alpha1.TailoredProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: tc.OperatorNamespace.Namespace, + Annotations: map[string]string{ + "compliance.openshift.io/product-type": "Node", + }, + }, + Spec: cmpv1alpha1.TailoredProfileSpec{ + Title: "CIS Kubelet Auto-Remediation Test Profile", + Description: "Test profile for validating KubeletConfig auto-remediation", + EnableRules: []cmpv1alpha1.RuleReferenceSpec{ + {Name: "ocp4-kubelet-configure-tls-cipher-suites"}, + }, + }, + } + + return c.Create(context.TODO(), tp) +}