diff --git a/CATALOG.md b/CATALOG.md index 281cd6731..a06745ca1 100644 --- a/CATALOG.md +++ b/CATALOG.md @@ -7,7 +7,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be ## Test cases summary -### Total test cases: 109 +### Total test cases: 113 ### Total suites: 10 @@ -19,7 +19,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be |manageability|2| |networking|11| |observability|4| -|operator|7| +|operator|11| |performance|6| |platform-alteration|13| |preflight|17| @@ -36,11 +36,11 @@ Depending on the workload type, not all tests are required to pass to satisfy be |---|---| |7|1| -### Non-Telco specific tests only: 62 +### Non-Telco specific tests only: 66 |Mandatory|Optional| |---|---| -|42|20| +|46|20| ### Telco specific tests only: 27 @@ -1122,6 +1122,22 @@ Tags|telco,observability ### operator +#### operator-automount-tokens + +Property|Description +---|--- +Unique ID|operator-automount-tokens +Description|Tests that check that the pods disable the automount service account token." +Suggested Remediation|Ensure that the pods have the automount service account token disabled. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + #### operator-crd-openapi-schema Property|Description @@ -1202,6 +1218,54 @@ Tags|common,operator |Non-Telco|Mandatory| |Telco|Mandatory| +#### operator-read-only-file-system + +Property|Description +---|--- +Unique ID|operator-read-only-file-system +Description|Tests that check that the pods have the read-only root filesystem setting enabled. +Suggested Remediation|Ensure that the pods have the read-only root filesystem setting enabled. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + +#### operator-run-as-non-root + +Property|Description +---|--- +Unique ID|operator-run-as-non-root +Description|Tests that checks the pods ensure they are run as non root. +Suggested Remediation|Ensure that the pods are running as non root. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + +#### operator-run-as-user-id + +Property|Description +---|--- +Unique ID|operator-run-as-user-id +Description|Tests that checks the user id of the pods ensure it is not 0. +Suggested Remediation|Ensure that the user ID of the pods is not 0. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + #### operator-semantic-versioning Property|Description diff --git a/cnf-certification-test/accesscontrol/suite.go b/cnf-certification-test/accesscontrol/suite.go index 8a4979879..c945484a8 100644 --- a/cnf-certification-test/accesscontrol/suite.go +++ b/cnf-certification-test/accesscontrol/suite.go @@ -23,10 +23,10 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/namespace" - "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/resources" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/securitycontextcontainer" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common" + "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/identifiers" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/networking/netutil" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/networking/services" diff --git a/cnf-certification-test/accesscontrol/rbac/automount.go b/cnf-certification-test/common/rbac/automount.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/automount.go rename to cnf-certification-test/common/rbac/automount.go diff --git a/cnf-certification-test/accesscontrol/rbac/automount_test.go b/cnf-certification-test/common/rbac/automount_test.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/automount_test.go rename to cnf-certification-test/common/rbac/automount_test.go diff --git a/cnf-certification-test/accesscontrol/rbac/roles.go b/cnf-certification-test/common/rbac/roles.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/roles.go rename to cnf-certification-test/common/rbac/roles.go diff --git a/cnf-certification-test/accesscontrol/rbac/roles_test.go b/cnf-certification-test/common/rbac/roles_test.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/roles_test.go rename to cnf-certification-test/common/rbac/roles_test.go diff --git a/cnf-certification-test/identifiers/doclinks.go b/cnf-certification-test/identifiers/doclinks.go index 650a9c418..4fc070a47 100644 --- a/cnf-certification-test/identifiers/doclinks.go +++ b/cnf-certification-test/identifiers/doclinks.go @@ -110,6 +110,10 @@ const ( TestOperatorCrdSchemaIdentifierDocLink = DocOperatorRequirement TestOperatorCrdVersioningIdentifierDocLink = DocOperatorRequirement TestOperatorSingleCrdOwnerIdentifierDocLink = DocOperatorRequirement + TestOperatorRunAsUserIDDocLink = DocOperatorRequirement + TestOperatorRunAsNonRootDocLink = DocOperatorRequirement + TestOperatorAutomountTokensDocLink = DocOperatorRequirement + TestOperatorReadOnlyFilesystemDocLink = DocOperatorRequirement // Observability Test Suite TestLoggingIdentifierDocLink = "https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-logging" diff --git a/cnf-certification-test/identifiers/identifiers.go b/cnf-certification-test/identifiers/identifiers.go index 8675ab52f..1c466d577 100644 --- a/cnf-certification-test/identifiers/identifiers.go +++ b/cnf-certification-test/identifiers/identifiers.go @@ -122,6 +122,10 @@ var ( TestHelmIsCertifiedIdentifier claim.Identifier TestOperatorIsInstalledViaOLMIdentifier claim.Identifier TestOperatorHasSemanticVersioningIdentifier claim.Identifier + TestOperatorReadOnlyFilesystem claim.Identifier + TestOperatorAutomountTokens claim.Identifier + TestOperatorRunAsNonRoot claim.Identifier + TestOperatorRunAsUserID claim.Identifier TestOperatorCrdVersioningIdentifier claim.Identifier TestOperatorCrdSchemaIdentifier claim.Identifier TestOperatorSingleCrdOwnerIdentifier claim.Identifier @@ -930,6 +934,70 @@ that Node's kernel may not have the same hacks.'`, }, TagCommon) + TestOperatorRunAsUserID = AddCatalogEntry( + "run-as-user-id", + common.OperatorTestKey, + `Tests that checks the user id of the pods ensure it is not 0.`, + OperatorRunAsUserID, + NoExceptions, + TestOperatorRunAsUserIDDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorRunAsNonRoot = AddCatalogEntry( + "run-as-non-root", + common.OperatorTestKey, + `Tests that checks the pods ensure they are run as non root.`, + OperatorRunAsNonRoot, + NoExceptions, + TestOperatorRunAsNonRootDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorAutomountTokens = AddCatalogEntry( + "automount-tokens", + common.OperatorTestKey, + `Tests that check that the pods disable the automount service account token."`, + OperatorAutomountTokens, + NoExceptions, + TestOperatorAutomountTokensDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorReadOnlyFilesystem = AddCatalogEntry( + "read-only-file-system", + common.OperatorTestKey, + `Tests that check that the pods have the read-only root filesystem setting enabled.`, + OperatorReadOnlyFilesystem, + NoExceptions, + TestOperatorReadOnlyFilesystemDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + TestOperatorCrdVersioningIdentifier = AddCatalogEntry( "crd-versioning", common.OperatorTestKey, diff --git a/cnf-certification-test/identifiers/remediation.go b/cnf-certification-test/identifiers/remediation.go index cdf57be9c..619cc9498 100644 --- a/cnf-certification-test/identifiers/remediation.go +++ b/cnf-certification-test/identifiers/remediation.go @@ -83,6 +83,14 @@ const ( OperatorCrdSchemaIdentifierRemediation = `Ensure that the Operator CRD is defined with OpenAPI spec.` + OperatorRunAsUserID = `Ensure that the user ID of the pods is not 0.` + + OperatorRunAsNonRoot = `Ensure that the pods are running as non root.` + + OperatorAutomountTokens = `Ensure that the pods have the automount service account token disabled.` + + OperatorReadOnlyFilesystem = `Ensure that the pods have the read-only root filesystem setting enabled.` + OperatorCrdVersioningRemediation = `Ensure that the Operator CRD has a valid version.` OperatorSingleCrdOwnerRemediation = `Ensure that a CRD is owned by only one Operator` diff --git a/cnf-certification-test/operator/helper.go b/cnf-certification-test/operator/helper.go new file mode 100644 index 000000000..793d81511 --- /dev/null +++ b/cnf-certification-test/operator/helper.go @@ -0,0 +1,47 @@ +// Copyright (C) 2020-2024 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +/* +Package operator provides CNFCERT tests used to validate operator CNF facets. +*/ + +package operator + +import "strings" + +// CsvResult holds the results of the splitCsv function. +type CsvResult struct { + NameCsv string + Namespace string +} + +// splitCsv splits the input string to extract namecsv and namespace. +func SplitCsv(csv string) CsvResult { + // Split by comma to separate components + parts := strings.Split(csv, ",") + var result CsvResult + + for _, part := range parts { + part = strings.TrimSpace(part) + + if strings.HasPrefix(part, "ns=") { + result.Namespace = strings.TrimPrefix(part, "ns=") + } else { + result.NameCsv = part + } + } + return result +} diff --git a/cnf-certification-test/operator/helper_test.go b/cnf-certification-test/operator/helper_test.go new file mode 100644 index 000000000..3f2f9a681 --- /dev/null +++ b/cnf-certification-test/operator/helper_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2020-2024 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +/* +Package operator provides CNFCERT tests used to validate operator CNF facets. +*/ + +package operator + +import "testing" + +func TestSplitCsv(t *testing.T) { + tests := []struct { + input string + expectedCsv string + expectedNs string + }{ + { + input: "hazelcast-platform-operator.v5.12.0, ns=tnf", + expectedCsv: "hazelcast-platform-operator.v5.12.0", + expectedNs: "tnf", + }, + { + input: "example-operator.v1.0.0, ns=example-ns", + expectedCsv: "example-operator.v1.0.0", + expectedNs: "example-ns", + }, + { + input: "another-operator.v2.3.1, ns=another-ns", + expectedCsv: "another-operator.v2.3.1", + expectedNs: "another-ns", + }, + { + input: "no-namespace", + expectedCsv: "no-namespace", + expectedNs: "", + }, + { + input: "ns=onlynamespace", + expectedCsv: "", + expectedNs: "onlynamespace", + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := SplitCsv(tt.input) + if result.NameCsv != tt.expectedCsv { + t.Errorf("splitCsv(%q) got namecsv %q, want %q", tt.input, result.NameCsv, tt.expectedCsv) + } + if result.Namespace != tt.expectedNs { + t.Errorf("splitCsv(%q) got namespace %q, want %q", tt.input, result.Namespace, tt.expectedNs) + } + }) + } +} diff --git a/cnf-certification-test/operator/suite.go b/cnf-certification-test/operator/suite.go index 5dfa3ed89..d7343229e 100644 --- a/cnf-certification-test/operator/suite.go +++ b/cnf-certification-test/operator/suite.go @@ -20,8 +20,10 @@ import ( "strings" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common" + "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/identifiers" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/operator/phasecheck" + "github.com/test-network-function/cnf-certification-test/internal/clientsholder" "github.com/test-network-function/cnf-certification-test/internal/log" "github.com/test-network-function/cnf-certification-test/pkg/checksdb" @@ -39,6 +41,7 @@ var ( } ) +//nolint:funlen func LoadChecks() { log.Debug("Loading %s suite checks", common.OperatorTestKey) @@ -93,6 +96,33 @@ func LoadChecks() { testOperatorSingleCrdOwner(c, &env) return nil })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorRunAsUserID)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorPodsRunAsUserID(c, &env) + return nil + })) + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorRunAsNonRoot)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorPodsRunAsNonRoot(c, &env) + return nil + })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorAutomountTokens)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorPodsAutomountTokens(c, &env) + return nil + })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorReadOnlyFilesystem)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorContainersReadOnlyFilesystem(c, &env) + return nil + })) } // This function check if the Operator CRD version follows K8s versioning @@ -317,3 +347,120 @@ func testOperatorSingleCrdOwner(check *checksdb.Check, env *provider.TestEnviron check.SetResult(compliantObjects, nonCompliantObjects) } + +func testOperatorPodsRunAsUserID(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + for csv, pods := range env.CSVToPodListMap { + CsvResult := SplitCsv(csv) + check.LogInfo("Name of csv: %q in namespaces: %q", CsvResult.NameCsv, CsvResult.Namespace) + for _, pod := range pods { + check.LogInfo("Testing Pod %q in namespace %q", pod.Name, pod.Namespace) + if pod.IsRunAsUserID(0) { + check.LogError("Non-compliant Pod %q in namespace %q: UserID is 0", pod.Name, pod.Namespace) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Pod has been found with UserID set to 0", false)) + } else { + check.LogInfo("Compliant Pod %q in namespace %q: UserID is not 0", pod.Name, pod.Namespace) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Pod has been found with UserID not set to 0", true)) + } + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorPodsRunAsNonRoot(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + var knownContainersToSkip = []string{"kube-rbac-proxy"} + + for csv, pods := range env.CSVToPodListMap { + CsvResult := SplitCsv(csv) + check.LogInfo("Name of csv: %q in namespaces: %q", CsvResult.NameCsv, CsvResult.Namespace) + for _, pod := range pods { + check.LogInfo("Testing Pod %q in namespace %q", pod.Name, pod.Namespace) + // We are looking through both the containers and the pods separately to make compliant and non-compliant objects. + for _, c := range pod.Containers { + skipKnownContainer := false + for _, k := range knownContainersToSkip { + if c.Name == k { + check.LogInfo("Skipping container %q in Pod %q", c.Name, pod.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(c.Namespace, c.Name, "Container is allowed to run as root", true)) + skipKnownContainer = true + break + } + } + + if skipKnownContainer { + continue + } + + if c.IsContainerRunAsNonRoot() { + check.LogInfo("Container %q in Pod %q is running as non-root", c.Name, pod.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(c.Namespace, c.Name, "Container is running as non-root", true)) + } else { + check.LogError("Container %q in Pod %q is running as root", c.Name, pod.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Container is running as root", false)) + } + } + + if pod.IsRunAsNonRoot() { + check.LogInfo("Pod %q is running as non-root", pod.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Pod is running as non-root", true)) + } else { + check.LogError("Pod %q is running as root", pod.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Pod is running as root", false)) + } + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorPodsAutomountTokens(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + for csv, pods := range env.CSVToPodListMap { + CsvResult := SplitCsv(csv) + check.LogInfo("Name of csv: %q in namespaces: %q", CsvResult.NameCsv, CsvResult.Namespace) + for _, pod := range pods { + check.LogInfo("Testing Pod %q in namespace %q", pod.Name, pod.Namespace) + // Evaluate the pod's automount service tokens and any attached service accounts + client := clientsholder.GetClientsHolder() + podPassed, newMsg := rbac.EvaluateAutomountTokens(client.K8sClient.CoreV1(), pod.Pod) + if !podPassed { + check.LogInfo("Pod %q in namespace %q has automount service account token set to false", pod.Name, pod.Namespace) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Pod has automount service account token set to false", true)) + } else { + check.LogError("Pod %q in namespace %q: %s", pod.Name, pod.Namespace, newMsg) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, newMsg, false)) + } + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorContainersReadOnlyFilesystem(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + for csv, pods := range env.CSVToPodListMap { + CsvResult := SplitCsv(csv) + check.LogInfo("Name of csv: %q in namespaces: %q", CsvResult.NameCsv, CsvResult.Namespace) + for _, pod := range pods { + check.LogInfo("Testing Pod %q in namespace %q", pod.Name, pod.Namespace) + for _, cut := range pod.Containers { + check.LogInfo("Testing Container %q in Pod %q", cut.Name, pod.Name) + if cut.IsReadOnlyRootFilesystem(check.GetLogger()) { + check.LogInfo("Container %q in Pod %q has a read-only root filesystem.", cut.Name, pod.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Container has a read-only root filesystem", true)) + } else { + check.LogError("Container %q in Pod %q does not have a read-only root filesystem.", cut.Name, pod.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(pod.Namespace, pod.Name, "Container does not have a read-only root filesystem", false)) + } + } + } + check.SetResult(compliantObjects, nonCompliantObjects) + } +} diff --git a/expected_results.yaml b/expected_results.yaml index a919c81c7..9e71dd297 100644 --- a/expected_results.yaml +++ b/expected_results.yaml @@ -55,11 +55,13 @@ testCases: - observability-crd-status - observability-pod-disruption-budget - observability-termination-policy + - operator-automount-tokens - operator-crd-openapi-schema - operator-crd-versioning - operator-install-source - operator-install-status-no-privileges - operator-install-status-succeeded + - operator-run-as-user-id - operator-semantic-versioning - operator-single-crd-owner - performance-exclusive-cpu-pool @@ -69,6 +71,8 @@ testCases: fail: - access-control-security-context # test pod does not meet the security requirements - affiliated-certification-container-is-certified-digest # test container image is not certified + - operator-read-only-file-system + - operator-run-as-non-root skip: - access-control-sys-ptrace-capability - affiliated-certification-helm-version diff --git a/pkg/provider/containers.go b/pkg/provider/containers.go index d53e21a04..64a35db95 100644 --- a/pkg/provider/containers.go +++ b/pkg/provider/containers.go @@ -185,3 +185,18 @@ func (c *Container) HasExecProbes() bool { func (c *Container) IsTagEmpty() bool { return c.ContainerImageIdentifier.Tag == "" } + +func (c *Container) IsReadOnlyRootFilesystem(logger *log.Logger) bool { + logger.Info("Testing Container %q", c) + if c.Container.SecurityContext == nil || c.Container.SecurityContext.ReadOnlyRootFilesystem == nil { + return false + } + return *c.Container.SecurityContext.ReadOnlyRootFilesystem +} + +func (c *Container) IsContainerRunAsNonRoot() bool { + if c.SecurityContext != nil && c.SecurityContext.RunAsNonRoot != nil { + return *c.SecurityContext.RunAsNonRoot + } + return false +} diff --git a/pkg/provider/containers_test.go b/pkg/provider/containers_test.go index af4779c46..b52cf048e 100644 --- a/pkg/provider/containers_test.go +++ b/pkg/provider/containers_test.go @@ -17,9 +17,11 @@ package provider import ( + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/test-network-function/cnf-certification-test/internal/log" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -204,3 +206,83 @@ func TestIsTagEmpty(t *testing.T) { assert.Equal(t, tc.expectedOutput, tc.testContainer.IsTagEmpty()) } } + +func TestIsreadOnlyRootFilessystem(t *testing.T) { + trueVal := true + falseVal := false + testCases := []struct { + testContainer Container + expectedOutput bool + }{ + { + testContainer: Container{ + Container: &corev1.Container{ + Name: "TestContainer1", + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: &trueVal, + }, + }, + }, + expectedOutput: true, + }, + { + testContainer: Container{ + Container: &corev1.Container{ + Name: "TestContainer2", + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: &falseVal, + }, + }, + }, + expectedOutput: false, + }, + } + + for _, tc := range testCases { + log.SetupLogger(os.Stdout, "INFO") + actualOutput := tc.testContainer.IsReadOnlyRootFilesystem(log.GetLogger()) + assert.Equal(t, tc.expectedOutput, actualOutput) + } +} + +func TestIsContainerRunAsNonRoot(t *testing.T) { + trueVal := true + falseVal := false + tests := []struct { + name string + container Container + expected bool + }{ + { + name: "Container set to run as non-root", + container: Container{ + Container: &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &trueVal, + }, + }, + }, + expected: true, + }, + { + name: "Container set to not run as non-root", + container: Container{ + Container: &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &falseVal, + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.container.IsContainerRunAsNonRoot() + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/pkg/provider/pods.go b/pkg/provider/pods.go index 52ac42d31..f386d2a00 100644 --- a/pkg/provider/pods.go +++ b/pkg/provider/pods.go @@ -26,6 +26,7 @@ import ( "github.com/test-network-function/cnf-certification-test/internal/clientsholder" "github.com/test-network-function/cnf-certification-test/internal/log" "github.com/test-network-function/cnf-certification-test/pkg/podhelper" + corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -404,6 +405,16 @@ func (p *Pod) IsRunAsUserID(uid int64) bool { return *p.Pod.Spec.SecurityContext.RunAsUser == uid } +func (p *Pod) IsRunAsNonRoot() bool { + // Check pod-level security context + if p.Pod.Spec.SecurityContext != nil && p.Pod.Spec.SecurityContext.RunAsNonRoot != nil { + return *p.Pod.Spec.SecurityContext.RunAsNonRoot + } + + // If neither container-level nor pod-level security context is set, fail + return false +} + // Get the list of top owners of pods func (p *Pod) GetTopOwner() (topOwners map[string]podhelper.TopOwner, err error) { return podhelper.GetPodTopOwner(p.Namespace, p.OwnerReferences) diff --git a/pkg/provider/pods_test.go b/pkg/provider/pods_test.go index 12d29d5b5..cf44df99d 100644 --- a/pkg/provider/pods_test.go +++ b/pkg/provider/pods_test.go @@ -517,3 +517,59 @@ func TestIsRunAsUserID(t *testing.T) { assert.Equal(t, tc.expectedOutput, tc.testPod.IsRunAsUserID(tc.testUID)) } } + +func TestIsRunAsNonRoot(t *testing.T) { + tests := []struct { + name string + pod *Pod + expected bool + }{ + { + name: "All containers and pod set to run as non-root", + pod: &Pod{ + Pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: boolPtr(true), + }, + }, + }, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: boolPtr(true), + }, + }, + }, + }, + expected: true, + }, + { + name: "No containers, pod set to run as non-root", + pod: &Pod{ + Pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: boolPtr(true), + }, + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.pod.IsRunAsNonRoot() + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Helper function to get a pointer to a bool value +func boolPtr(b bool) *bool { + return &b +}