From 6f4d882d7e28d53b2dc43970b1a0b59219796299 Mon Sep 17 00:00:00 2001 From: mjshastha Date: Mon, 10 Jul 2023 14:22:25 +0530 Subject: [PATCH 1/2] Our objective for this task is to align the titles and logic with the commercial version. --- pkg/scanners/helm/test/scanner_test.go | 16 +-- .../default_namespace_should_not_be_used.rego | 7 +- .../capabilities_no_drop_at_least_one.rego | 2 +- .../advanced/optional/manages_etc_hosts.rego | 2 +- .../allowing_to_update_a_malicious_pod.rego | 8 +- ...lowing_to_update_a_malicious_pod_test.rego | 72 +++++++++++ .../general/capabilities_no_drop_all.rego | 2 +- .../policies/general/delete_pod_logs.rego | 2 +- .../policies/general/get_shell_on_pod.rego | 25 ++-- .../general/get_shell_on_pod_test.rego | 85 ++++--------- .../policies/general/manage_configmaps.rego | 2 +- .../general/manage_kubernetes_networking.rego | 2 +- .../manage_kubernetes_rbac_resources.rego | 2 +- .../general/runs_with_GID_le_10000.rego | 2 +- .../general/runs_with_UID_le_10000.rego | 2 +- .../general/uses_image_tag_latest.rego | 2 +- .../pss/baseline/10_windows_host_process.rego | 2 +- .../11_seccomp_profile_unconfined.rego | 47 +++++-- .../11_seccomp_profile_unconfined_test.rego | 120 +++--------------- .../policies/pss/baseline/2_privileged.rego | 2 +- .../3_specific_capabilities_added.rego | 2 +- .../baseline/6_apparmor_policy_disabled.rego | 2 +- .../restricted/1_non_core_volume_types.rego | 3 +- .../2_can_elevate_its_own_privileges.rego | 2 +- ...ntime_default_seccomp_profile_not_set.rego | 2 +- ...dmin_role_is_only_used_where_required.rego | 9 +- ...role_is_only_used_where_required_test.rego | 46 +++++++ 27 files changed, 248 insertions(+), 222 deletions(-) diff --git a/pkg/scanners/helm/test/scanner_test.go b/pkg/scanners/helm/test/scanner_test.go index eb89a1778..7998c4a79 100644 --- a/pkg/scanners/helm/test/scanner_test.go +++ b/pkg/scanners/helm/test/scanner_test.go @@ -47,7 +47,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 12, len(failed)) + assert.Equal(t, 13, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -58,7 +58,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 12) + assert.Len(t, errorCodes, 13) sort.Strings(errorCodes) @@ -67,7 +67,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", }, errorCodes) } } @@ -127,7 +127,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 12, len(failed)) + assert.Equal(t, 13, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -146,7 +146,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", }, errorCodes) } } @@ -213,7 +213,7 @@ deny[res] { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 14, len(failed)) + assert.Equal(t, 15, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -224,7 +224,7 @@ deny[res] { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 13) + assert.Len(t, errorCodes, 14) sort.Strings(errorCodes) @@ -233,7 +233,7 @@ deny[res] { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", "AVD-USR-ID001", + "AVD-KSV-0104", "AVD-KSV-0106", "AVD-USR-ID001", }, errorCodes) }) } diff --git a/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego b/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego index 6a5ba35df..bccf5bc78 100644 --- a/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego +++ b/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego @@ -1,5 +1,5 @@ # METADATA -# title: "The default namespace should not be used" +# title: "Workloads in the default namespace" # description: "ensure that default namespace should not be used" # scope: package # schemas: @@ -21,12 +21,15 @@ import data.lib.kubernetes default defaultNamespaceInUse = false +allowedKinds := ["pod", "replicaset", "replicationcontroller", "deployment", "statefulset", "daemonset", "cronjob", "job"] + defaultNamespaceInUse { kubernetes.namespace == "default" + lower(kubernetes.kind) == allowedKinds[_] } deny[res] { defaultNamespaceInUse - msg := sprintf("%s '%s' should not be set with 'default' namespace", [kubernetes.kind, kubernetes.name]) + msg := kubernetes.format(sprintf("%s %s in %s namespace should set metadata.namespace to a non-default namespace", [lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) res := result.new(msg, input.metadata.namespace) } diff --git a/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego b/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego index 539ad2a32..ca3a78021 100644 --- a/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego +++ b/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Unused capabilities should be dropped (drop any)" +# title: "Default capabilities: some containers do not drop any" # description: "Security best practices require containers to run with minimal required capabilities." # scope: package # schemas: diff --git a/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego b/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego index 22795cce9..4ee8c3aa7 100644 --- a/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego +++ b/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego @@ -1,5 +1,5 @@ # METADATA -# title: "hostAliases is set" +# title: "Manages /etc/hosts" # description: "Managing /etc/hosts aliases can prevent the container engine from modifying the file after a pod’s containers have already been started." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego index 4afb0edee..e3272a18d 100644 --- a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego +++ b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow update/create of a malicious pod" +# title: "Manage Kubernetes workloads and pods" # description: "Check whether role permits update/create of a malicious pod" # scope: package # schemas: @@ -20,9 +20,9 @@ package builtin.kubernetes.KSV048 import data.lib.kubernetes import data.lib.utils -workloads := ["deployments", "daemonsets", "statefulsets", "replicationcontrollers", "replicasets", "jobs", "cronjobs"] +workloads := ["pods", "deployments", "jobs", "cronjobs", "statefulsets", "daemonsets", "replicasets", "replicationcontrollers"] -changeVerbs := ["update", "create", "*"] +changeVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] readKinds := ["Role", "ClusterRole"] @@ -35,6 +35,6 @@ update_malicious_pod[input.rules[ru]] { deny[res] { badRule := update_malicious_pod[_] - msg := "Role permits create/update of a malicious pod" + msg := kubernetes.format(sprintf("%s '%s' should not have access to resources %s for verbs %s", [kubernetes.kind, kubernetes.name, workloads, changeVerbs])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego index 4122bfe57..912a983ea 100644 --- a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego +++ b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego @@ -251,3 +251,75 @@ test_update_malicious_pod_cronjobs { count(r) > 0 } + +test_update_malicious_pod_deletecollection { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["cronjobs"], + "verbs": ["deletecollection"], + }], + } + + count(r) > 0 +} + +test_update_malicious_pod_delete { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["delete"], + }], + } + + count(r) == 0 +} + +test_update_malicious_pod_patch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["patch"], + }], + } + + count(r) == 0 +} + +test_update_malicious_pod_impersonate { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["impersonate"], + }], + } + + count(r) == 0 +} diff --git a/rules/kubernetes/policies/general/capabilities_no_drop_all.rego b/rules/kubernetes/policies/general/capabilities_no_drop_all.rego index 0e1de81a9..cdb9c6502 100644 --- a/rules/kubernetes/policies/general/capabilities_no_drop_all.rego +++ b/rules/kubernetes/policies/general/capabilities_no_drop_all.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default capabilities not dropped" +# title: "Default capabilities: some containers do not drop all" # description: "The container should drop all default capabilities and add only those that are needed for its execution." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/delete_pod_logs.rego b/rules/kubernetes/policies/general/delete_pod_logs.rego index 9c0cd3d12..6c58e04f5 100644 --- a/rules/kubernetes/policies/general/delete_pod_logs.rego +++ b/rules/kubernetes/policies/general/delete_pod_logs.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow deletion of pod logs" +# title: "Delete pod logs" # description: "Used to cover attacker’s tracks, but most clusters ship logs quickly off-cluster." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/get_shell_on_pod.rego b/rules/kubernetes/policies/general/get_shell_on_pod.rego index e1a4b1690..d88cdc310 100644 --- a/rules/kubernetes/policies/general/get_shell_on_pod.rego +++ b/rules/kubernetes/policies/general/get_shell_on_pod.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow getting shell on pods" +# title: "Exec into Pods" # description: "Check whether role permits getting shell on pods" # scope: package # schemas: @@ -20,24 +20,21 @@ package builtin.kubernetes.KSV053 import data.lib.kubernetes import data.lib.utils +workloads := ["pods/exec"] + +changeVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] + readKinds := ["Role", "ClusterRole"] -get_shell_on_pod[ruleA] { +execPodsRestricted[input.rules[ru]] { + some ru, r, v input.kind == readKinds[_] - some i, j - ruleA := input.rules[i] - ruleB := input.rules[j] - i < j - ruleA.apiGroups[_] == "*" - ruleA.resources[_] == "pods/exec" - ruleA.verbs[_] == "create" - ruleB.apiGroups[_] == "*" - ruleB.resources[_] == "pods" - ruleB.verbs[_] == "get" + input.rules[ru].resources[r] == workloads[_] + input.rules[ru].verbs[v] == changeVerbs[_] } deny[res] { - badRule := get_shell_on_pod[_] - msg := "Role permits getting shell on pods" + badRule := execPodsRestricted[_] + msg := kubernetes.format(sprintf("%s '%s' should not have access to resource '%s' for verbs %s", [kubernetes.kind, kubernetes.name, workloads, changeVerbs])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/get_shell_on_pod_test.rego b/rules/kubernetes/policies/general/get_shell_on_pod_test.rego index 98e978c86..6454fdeb6 100644 --- a/rules/kubernetes/policies/general/get_shell_on_pod_test.rego +++ b/rules/kubernetes/policies/general/get_shell_on_pod_test.rego @@ -8,18 +8,11 @@ test_getting_shell_on_pods { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create"], + }], } count(r) == 1 @@ -33,18 +26,11 @@ test_getting_shell_on_pods_no_pod_exec { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec1"], - "verbs": ["create"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec1"], + "verbs": ["create"], + }], } count(r) == 0 @@ -58,18 +44,11 @@ test_getting_shell_on_pods_no_verb_create { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 @@ -83,18 +62,11 @@ test_getting_shell_on_pods_no_resource_pod { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods1"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 @@ -108,18 +80,11 @@ test_getting_shell_on_pods_no_verb_get { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get1"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 diff --git a/rules/kubernetes/policies/general/manage_configmaps.rego b/rules/kubernetes/policies/general/manage_configmaps.rego index b077240d6..d52f8dbd0 100644 --- a/rules/kubernetes/policies/general/manage_configmaps.rego +++ b/rules/kubernetes/policies/general/manage_configmaps.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of configmaps" +# title: "Manage configmaps" # description: "Some workloads leverage configmaps to store sensitive data or configuration parameters that affect runtime behavior that can be modified by an attacker or combined with another issue to potentially lead to compromise." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/manage_kubernetes_networking.rego b/rules/kubernetes/policies/general/manage_kubernetes_networking.rego index 1f898ce7c..6c8ca5af8 100644 --- a/rules/kubernetes/policies/general/manage_kubernetes_networking.rego +++ b/rules/kubernetes/policies/general/manage_kubernetes_networking.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of networking resources" +# title: "Manage Kubernetes networking" # description: "The ability to control which pods get service traffic directed to them allows for interception attacks. Controlling network policy allows for bypassing lateral movement restrictions." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego b/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego index dcfe391fb..cba4559db 100644 --- a/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego +++ b/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of RBAC resources" +# title: "Manage Kubernetes RBAC resources" # description: "An effective level of access equivalent to cluster-admin should not be provided." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego b/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego index 8b678e703..7f1fc49cc 100644 --- a/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego +++ b/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Runs with low group ID" +# title: "Runs with GID <= 10000" # description: "Force the container to run with group ID > 10000 to avoid conflicts with the host’s user table." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego b/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego index f4ab333f7..87a82fbb3 100644 --- a/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego +++ b/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Runs with low user ID" +# title: "Runs with UID <= 10000" # description: "Force the container to run with user ID > 10000 to avoid conflicts with the host’s user table." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/uses_image_tag_latest.rego b/rules/kubernetes/policies/general/uses_image_tag_latest.rego index e4fef96e9..1cf0fe107 100644 --- a/rules/kubernetes/policies/general/uses_image_tag_latest.rego +++ b/rules/kubernetes/policies/general/uses_image_tag_latest.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Image tag ':latest' used" +# title: "Image tag \":latest\" used" # description: "It is best to avoid using the ':latest' image tag when deploying containers in production. Doing so makes it hard to track which version of the image is running, and hard to roll back the version." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego b/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego index 220615fa1..155377d46 100644 --- a/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego +++ b/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego @@ -1,5 +1,5 @@ # METADATA -# title: "HostProcess container defined" +# title: "Access to host process" # description: "Windows pods offer the ability to run HostProcess containers which enable privileged access to the Windows node." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego index 8fea9def4..e7dcacc9a 100644 --- a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego +++ b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Seccomp profile unconfined" +# title: "Seccomp policies disabled" # description: "Seccomp profile must not be explicitly set to 'Unconfined'." # scope: package # schemas: @@ -20,20 +20,47 @@ package builtin.kubernetes.KSV104 import data.lib.kubernetes import data.lib.utils -failSeccomp[profile] { - spec := input.spec - profile := spec.securityContext.seccompProfile - profile.type == "Unconfined" +# getSeccompContainers returns all containers which have a seccomp +# profile set and is profile not set to "unconfined" +getSeccompContainers[container] { + some i + keys := [key | key := sprintf("%s/%s", [ + "container.seccomp.security.alpha.kubernetes.io", + kubernetes.containers[_].name, + ])] + seccomp := object.filter(kubernetes.annotations[_], keys) + val := seccomp[i] + val != "unconfined" + [a, c] := split(i, "/") + container = c } -failSeccomp[profile] { +# getNoSeccompContainers returns all containers which do not have +# a seccomp profile specified or profile set to "unconfined" +getNoSeccompContainers[container] { + container := kubernetes.containers[_].name + not getSeccompContainers[container] +} + +# getContainersWithDisallowedSeccompProfileType returns all containers which have a seccomp +# profile set and is profile set to "Unconfined" +getContainersWithDisallowedSeccompProfileType[name] { + container := kubernetes.containers[_] + type := container.securityContext.seccompProfile.type + type == "Unconfined" + name = container.name +} + +# getContainersWithDisallowedSeccompProfileType returns all containers which do not have +# a seccomp profile type specified +getContainersWithDisallowedSeccompProfileType[name] { container := kubernetes.containers[_] - profile := container.securityContext.seccompProfile - profile.type == "Unconfined" + not container.securityContext.seccompProfile.type + name = container.name } deny[res] { - cause := failSeccomp[_] - msg := "You should not set Seccomp profile to 'Unconfined'." + cause := getContainersWithDisallowedSeccompProfileType[_] + msg := kubernetes.format(sprintf("container %s of %s %s in %s namespace should specify a seccomp profile", [getNoSeccompContainers[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) res := result.new(msg, cause) } diff --git a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego index c26fd4f3a..9afdf4224 100644 --- a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego +++ b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego @@ -1,128 +1,42 @@ package builtin.kubernetes.KSV104 -test_base_securityContext_seccompProfile_unconfined_denied { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {"type": "Unconfined"}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 1 - r[_].msg == "You should not set Seccomp profile to 'Unconfined'." -} - -test_base_securityContext_seccompProfile_unspecified_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 0 -} - -test_base_securityContext_seccompProfile_RuntimeDefault_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 0 -} - -test_container_securityContext_seccompProfile_unconfined_denied { +test_container_seccomp_profile_unconfined_denied { r := deny with input as { "apiVersion": "v1", "kind": "Pod", "metadata": {"name": "hello-sysctls"}, "spec": {"containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", "name": "hello", - "securityContext": {"seccompProfile": {"type": "Unconfined"}}, - }]}, - } - - count(r) == 1 - r[_].msg == "You should not set Seccomp profile to 'Unconfined'." -} - -test_container_securityContext_seccompProfile_unspecified_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": {"containers": [{ + "image": "busybox", "command": [ "sh", "-c", "echo 'Hello' && sleep 1h", ], - "image": "busybox", - "name": "hello", - "securityContext": {"seccompProfile": {}}, + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, }]}, } count(r) == 0 } -test_container_securityContext_seccompProfile_RuntimeDefault_allowed { +test_container_seccomp_profile_unconfined_allowed { r := deny with input as { "apiVersion": "v1", "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": {"containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, - }]}, + "metadata": {"name": "my-pod"}, + "spec": {"containers": [ + { + "name": "container-1", + "image": "nginx", + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + { + "name": "container-2", + "image": "busybox", + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + ]}, } count(r) == 0 diff --git a/rules/kubernetes/policies/pss/baseline/2_privileged.rego b/rules/kubernetes/policies/pss/baseline/2_privileged.rego index 99fea8dd4..84ea914ff 100644 --- a/rules/kubernetes/policies/pss/baseline/2_privileged.rego +++ b/rules/kubernetes/policies/pss/baseline/2_privileged.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Privileged container" +# title: "Privileged" # description: "Privileged containers share namespaces with the host system and do not offer any security. They should be used exclusively for system containers that require high privileges." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego b/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego index 6530cae8b..026897365 100644 --- a/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego +++ b/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Non-default capabilities added" +# title: "Specific capabilities added" # description: "Adding NET_RAW or capabilities beyond the default set must be disallowed." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego b/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego index 655ffb8d6..5b56e6cec 100644 --- a/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego +++ b/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default AppArmor profile not set" +# title: "Runtime/Default AppArmor profile not set" # description: "A program inside the container can bypass AppArmor protection policies." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego b/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego index 84f1e1a16..195c09cca 100644 --- a/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego +++ b/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Non-ephemeral volume types used" +# title: "Non-core volume types used." # description: "In addition to restricting HostPath volumes, usage of non-ephemeral volume types should be limited to those defined through PersistentVolumes." # scope: package # schemas: @@ -42,6 +42,7 @@ disallowed_volume_types = [ "portworxVolume", "scaleIO", "storageos", + "csi", ] # getDisallowedVolumes returns a list of volume names diff --git a/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego b/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego index a5d41509e..30ba9be21 100644 --- a/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego +++ b/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Process can elevate its own privileges" +# title: "Can elevate its own privileges" # description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego b/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego index b44a1ad7c..f4ca8ba86 100644 --- a/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego +++ b/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default Seccomp profile not set" +# title: "Runtime/Default Seccomp profile not set" # description: "The RuntimeDefault/Localhost seccomp profile must be required, or allow specific additional profiles." # scope: package # schemas: diff --git a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego index 69a8459c3..592f8206c 100644 --- a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego +++ b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Ensure that the cluster-admin role is only used where required" +# title: "User with admin access" # description: "The RBAC role cluster-admin provides wide-ranging powers over the environment and should be used only where and when needed." # scope: package # schemas: @@ -19,16 +19,17 @@ package builtin.kubernetes.KSV111 import data.lib.kubernetes +readRoleRefs := ["cluster-admin", "admin", "edit"] + roleBindings := ["clusterrolebinding", "rolebinding"] clusterAdminRoleInUse(roleBinding) { lower(kubernetes.kind) == roleBindings[_] - roleBinding.roleRef.name == "cluster-admin" - roleBinding.subjects[_].name != "system:masters" + roleBinding.roleRef.name == readRoleRefs[_] } deny[res] { clusterAdminRoleInUse(input) - msg := sprintf("%s '%s' with role 'cluster-admin' should be used only when required", [kubernetes.kind, kubernetes.name]) + msg := kubernetes.format(sprintf("%s '%s' should not bind to roles %s", [kubernetes.kind, kubernetes.name, readRoleRefs])) res := result.new(msg, input.metadata) } diff --git a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego index 8c1e4a799..d33e5207c 100644 --- a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego +++ b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego @@ -43,5 +43,51 @@ test_cluster_role_admin__used_with_system_role_binding { }, } + count(r) == 1 +} + +test_admin_used_with_system_role_binding { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "system:read-pods", + "namespace": "default", + }, + "subjects": [{ + "kind": "User", + "name": "system:masters", + "apiGroup": "rbac.authorization.k8s.io", + }], + "roleRef": { + "kind": "Role", + "name": "admin", + "apiGroup": "rbac.authorization.k8s.io", + }, + } + + count(r) == 1 +} + +test_non_role_ref_used_with_system_role_binding { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "system:read-pods", + "namespace": "default", + }, + "subjects": [{ + "kind": "User", + "name": "system:masters", + "apiGroup": "rbac.authorization.k8s.io", + }], + "roleRef": { + "kind": "Role", + "name": "admin1", + "apiGroup": "rbac.authorization.k8s.io", + }, + } + count(r) == 0 } From 5347284e1bb5444e98d4ea467cd8f7160f741d53 Mon Sep 17 00:00:00 2001 From: mjshastha Date: Mon, 10 Jul 2023 14:22:25 +0530 Subject: [PATCH 2/2] Our objective for this task is to align the titles and logic with the commercial version. --- pkg/scanners/helm/test/scanner_test.go | 16 +-- .../default_namespace_should_not_be_used.rego | 7 +- .../capabilities_no_drop_at_least_one.rego | 2 +- .../advanced/optional/manages_etc_hosts.rego | 2 +- .../allowing_to_update_a_malicious_pod.rego | 8 +- ...lowing_to_update_a_malicious_pod_test.rego | 72 +++++++++++ .../general/capabilities_no_drop_all.rego | 2 +- .../policies/general/delete_pod_logs.rego | 2 +- .../policies/general/get_shell_on_pod.rego | 25 ++-- .../general/get_shell_on_pod_test.rego | 85 ++++--------- .../policies/general/manage_configmaps.rego | 2 +- .../general/manage_kubernetes_networking.rego | 2 +- .../manage_kubernetes_rbac_resources.rego | 2 +- .../general/runs_with_GID_le_10000.rego | 2 +- .../general/runs_with_UID_le_10000.rego | 2 +- .../general/uses_image_tag_latest.rego | 2 +- .../pss/baseline/10_windows_host_process.rego | 2 +- .../11_seccomp_profile_unconfined.rego | 47 +++++-- .../11_seccomp_profile_unconfined_test.rego | 120 +++--------------- .../policies/pss/baseline/2_privileged.rego | 2 +- .../3_specific_capabilities_added.rego | 2 +- .../baseline/6_apparmor_policy_disabled.rego | 2 +- .../restricted/1_non_core_volume_types.rego | 3 +- .../2_can_elevate_its_own_privileges.rego | 2 +- ...ntime_default_seccomp_profile_not_set.rego | 2 +- ...dmin_role_is_only_used_where_required.rego | 9 +- ...role_is_only_used_where_required_test.rego | 46 +++++++ 27 files changed, 248 insertions(+), 222 deletions(-) diff --git a/pkg/scanners/helm/test/scanner_test.go b/pkg/scanners/helm/test/scanner_test.go index eb89a1778..7998c4a79 100644 --- a/pkg/scanners/helm/test/scanner_test.go +++ b/pkg/scanners/helm/test/scanner_test.go @@ -47,7 +47,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 12, len(failed)) + assert.Equal(t, 13, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -58,7 +58,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 12) + assert.Len(t, errorCodes, 13) sort.Strings(errorCodes) @@ -67,7 +67,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", }, errorCodes) } } @@ -127,7 +127,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 12, len(failed)) + assert.Equal(t, 13, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -146,7 +146,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", }, errorCodes) } } @@ -213,7 +213,7 @@ deny[res] { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 14, len(failed)) + assert.Equal(t, 15, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -224,7 +224,7 @@ deny[res] { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 13) + assert.Len(t, errorCodes, 14) sort.Strings(errorCodes) @@ -233,7 +233,7 @@ deny[res] { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0106", "AVD-USR-ID001", + "AVD-KSV-0104", "AVD-KSV-0106", "AVD-USR-ID001", }, errorCodes) }) } diff --git a/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego b/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego index 6a5ba35df..bccf5bc78 100644 --- a/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego +++ b/rules/kubernetes/policies/advanced/default_namespace_should_not_be_used.rego @@ -1,5 +1,5 @@ # METADATA -# title: "The default namespace should not be used" +# title: "Workloads in the default namespace" # description: "ensure that default namespace should not be used" # scope: package # schemas: @@ -21,12 +21,15 @@ import data.lib.kubernetes default defaultNamespaceInUse = false +allowedKinds := ["pod", "replicaset", "replicationcontroller", "deployment", "statefulset", "daemonset", "cronjob", "job"] + defaultNamespaceInUse { kubernetes.namespace == "default" + lower(kubernetes.kind) == allowedKinds[_] } deny[res] { defaultNamespaceInUse - msg := sprintf("%s '%s' should not be set with 'default' namespace", [kubernetes.kind, kubernetes.name]) + msg := kubernetes.format(sprintf("%s %s in %s namespace should set metadata.namespace to a non-default namespace", [lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) res := result.new(msg, input.metadata.namespace) } diff --git a/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego b/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego index 539ad2a32..ca3a78021 100644 --- a/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego +++ b/rules/kubernetes/policies/advanced/optional/capabilities_no_drop_at_least_one.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Unused capabilities should be dropped (drop any)" +# title: "Default capabilities: some containers do not drop any" # description: "Security best practices require containers to run with minimal required capabilities." # scope: package # schemas: diff --git a/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego b/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego index 22795cce9..4ee8c3aa7 100644 --- a/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego +++ b/rules/kubernetes/policies/advanced/optional/manages_etc_hosts.rego @@ -1,5 +1,5 @@ # METADATA -# title: "hostAliases is set" +# title: "Manages /etc/hosts" # description: "Managing /etc/hosts aliases can prevent the container engine from modifying the file after a pod’s containers have already been started." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego index 4afb0edee..e3272a18d 100644 --- a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego +++ b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow update/create of a malicious pod" +# title: "Manage Kubernetes workloads and pods" # description: "Check whether role permits update/create of a malicious pod" # scope: package # schemas: @@ -20,9 +20,9 @@ package builtin.kubernetes.KSV048 import data.lib.kubernetes import data.lib.utils -workloads := ["deployments", "daemonsets", "statefulsets", "replicationcontrollers", "replicasets", "jobs", "cronjobs"] +workloads := ["pods", "deployments", "jobs", "cronjobs", "statefulsets", "daemonsets", "replicasets", "replicationcontrollers"] -changeVerbs := ["update", "create", "*"] +changeVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] readKinds := ["Role", "ClusterRole"] @@ -35,6 +35,6 @@ update_malicious_pod[input.rules[ru]] { deny[res] { badRule := update_malicious_pod[_] - msg := "Role permits create/update of a malicious pod" + msg := kubernetes.format(sprintf("%s '%s' should not have access to resources %s for verbs %s", [kubernetes.kind, kubernetes.name, workloads, changeVerbs])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego index 4122bfe57..912a983ea 100644 --- a/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego +++ b/rules/kubernetes/policies/general/allowing_to_update_a_malicious_pod_test.rego @@ -251,3 +251,75 @@ test_update_malicious_pod_cronjobs { count(r) > 0 } + +test_update_malicious_pod_deletecollection { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["cronjobs"], + "verbs": ["deletecollection"], + }], + } + + count(r) > 0 +} + +test_update_malicious_pod_delete { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["delete"], + }], + } + + count(r) == 0 +} + +test_update_malicious_pod_patch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["patch"], + }], + } + + count(r) == 0 +} + +test_update_malicious_pod_impersonate { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["job"], + "verbs": ["impersonate"], + }], + } + + count(r) == 0 +} diff --git a/rules/kubernetes/policies/general/capabilities_no_drop_all.rego b/rules/kubernetes/policies/general/capabilities_no_drop_all.rego index 0e1de81a9..cdb9c6502 100644 --- a/rules/kubernetes/policies/general/capabilities_no_drop_all.rego +++ b/rules/kubernetes/policies/general/capabilities_no_drop_all.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default capabilities not dropped" +# title: "Default capabilities: some containers do not drop all" # description: "The container should drop all default capabilities and add only those that are needed for its execution." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/delete_pod_logs.rego b/rules/kubernetes/policies/general/delete_pod_logs.rego index 9c0cd3d12..6c58e04f5 100644 --- a/rules/kubernetes/policies/general/delete_pod_logs.rego +++ b/rules/kubernetes/policies/general/delete_pod_logs.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow deletion of pod logs" +# title: "Delete pod logs" # description: "Used to cover attacker’s tracks, but most clusters ship logs quickly off-cluster." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/get_shell_on_pod.rego b/rules/kubernetes/policies/general/get_shell_on_pod.rego index e1a4b1690..d88cdc310 100644 --- a/rules/kubernetes/policies/general/get_shell_on_pod.rego +++ b/rules/kubernetes/policies/general/get_shell_on_pod.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow getting shell on pods" +# title: "Exec into Pods" # description: "Check whether role permits getting shell on pods" # scope: package # schemas: @@ -20,24 +20,21 @@ package builtin.kubernetes.KSV053 import data.lib.kubernetes import data.lib.utils +workloads := ["pods/exec"] + +changeVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] + readKinds := ["Role", "ClusterRole"] -get_shell_on_pod[ruleA] { +execPodsRestricted[input.rules[ru]] { + some ru, r, v input.kind == readKinds[_] - some i, j - ruleA := input.rules[i] - ruleB := input.rules[j] - i < j - ruleA.apiGroups[_] == "*" - ruleA.resources[_] == "pods/exec" - ruleA.verbs[_] == "create" - ruleB.apiGroups[_] == "*" - ruleB.resources[_] == "pods" - ruleB.verbs[_] == "get" + input.rules[ru].resources[r] == workloads[_] + input.rules[ru].verbs[v] == changeVerbs[_] } deny[res] { - badRule := get_shell_on_pod[_] - msg := "Role permits getting shell on pods" + badRule := execPodsRestricted[_] + msg := kubernetes.format(sprintf("%s '%s' should not have access to resource '%s' for verbs %s", [kubernetes.kind, kubernetes.name, workloads, changeVerbs])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/get_shell_on_pod_test.rego b/rules/kubernetes/policies/general/get_shell_on_pod_test.rego index 98e978c86..6454fdeb6 100644 --- a/rules/kubernetes/policies/general/get_shell_on_pod_test.rego +++ b/rules/kubernetes/policies/general/get_shell_on_pod_test.rego @@ -8,18 +8,11 @@ test_getting_shell_on_pods { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create"], + }], } count(r) == 1 @@ -33,18 +26,11 @@ test_getting_shell_on_pods_no_pod_exec { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec1"], - "verbs": ["create"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec1"], + "verbs": ["create"], + }], } count(r) == 0 @@ -58,18 +44,11 @@ test_getting_shell_on_pods_no_verb_create { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 @@ -83,18 +62,11 @@ test_getting_shell_on_pods_no_resource_pod { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods1"], - "verbs": ["get"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 @@ -108,18 +80,11 @@ test_getting_shell_on_pods_no_verb_get { "namespace": "default", "name": "pod-reader", }, - "rules": [ - { - "apiGroups": ["*"], - "resources": ["pods/exec"], - "verbs": ["create1"], - }, - { - "apiGroups": ["*"], - "resources": ["pods"], - "verbs": ["get1"], - }, - ], + "rules": [{ + "apiGroups": ["*"], + "resources": ["pods/exec"], + "verbs": ["create1"], + }], } count(r) == 0 diff --git a/rules/kubernetes/policies/general/manage_configmaps.rego b/rules/kubernetes/policies/general/manage_configmaps.rego index b077240d6..d52f8dbd0 100644 --- a/rules/kubernetes/policies/general/manage_configmaps.rego +++ b/rules/kubernetes/policies/general/manage_configmaps.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of configmaps" +# title: "Manage configmaps" # description: "Some workloads leverage configmaps to store sensitive data or configuration parameters that affect runtime behavior that can be modified by an attacker or combined with another issue to potentially lead to compromise." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/manage_kubernetes_networking.rego b/rules/kubernetes/policies/general/manage_kubernetes_networking.rego index 1f898ce7c..6c8ca5af8 100644 --- a/rules/kubernetes/policies/general/manage_kubernetes_networking.rego +++ b/rules/kubernetes/policies/general/manage_kubernetes_networking.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of networking resources" +# title: "Manage Kubernetes networking" # description: "The ability to control which pods get service traffic directed to them allows for interception attacks. Controlling network policy allows for bypassing lateral movement restrictions." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego b/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego index dcfe391fb..cba4559db 100644 --- a/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego +++ b/rules/kubernetes/policies/general/manage_kubernetes_rbac_resources.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Do not allow management of RBAC resources" +# title: "Manage Kubernetes RBAC resources" # description: "An effective level of access equivalent to cluster-admin should not be provided." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego b/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego index 8b678e703..7f1fc49cc 100644 --- a/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego +++ b/rules/kubernetes/policies/general/runs_with_GID_le_10000.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Runs with low group ID" +# title: "Runs with GID <= 10000" # description: "Force the container to run with group ID > 10000 to avoid conflicts with the host’s user table." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego b/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego index f4ab333f7..87a82fbb3 100644 --- a/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego +++ b/rules/kubernetes/policies/general/runs_with_UID_le_10000.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Runs with low user ID" +# title: "Runs with UID <= 10000" # description: "Force the container to run with user ID > 10000 to avoid conflicts with the host’s user table." # scope: package # schemas: diff --git a/rules/kubernetes/policies/general/uses_image_tag_latest.rego b/rules/kubernetes/policies/general/uses_image_tag_latest.rego index e4fef96e9..1cf0fe107 100644 --- a/rules/kubernetes/policies/general/uses_image_tag_latest.rego +++ b/rules/kubernetes/policies/general/uses_image_tag_latest.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Image tag ':latest' used" +# title: "Image tag \":latest\" used" # description: "It is best to avoid using the ':latest' image tag when deploying containers in production. Doing so makes it hard to track which version of the image is running, and hard to roll back the version." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego b/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego index 220615fa1..155377d46 100644 --- a/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego +++ b/rules/kubernetes/policies/pss/baseline/10_windows_host_process.rego @@ -1,5 +1,5 @@ # METADATA -# title: "HostProcess container defined" +# title: "Access to host process" # description: "Windows pods offer the ability to run HostProcess containers which enable privileged access to the Windows node." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego index 8fea9def4..e7dcacc9a 100644 --- a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego +++ b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Seccomp profile unconfined" +# title: "Seccomp policies disabled" # description: "Seccomp profile must not be explicitly set to 'Unconfined'." # scope: package # schemas: @@ -20,20 +20,47 @@ package builtin.kubernetes.KSV104 import data.lib.kubernetes import data.lib.utils -failSeccomp[profile] { - spec := input.spec - profile := spec.securityContext.seccompProfile - profile.type == "Unconfined" +# getSeccompContainers returns all containers which have a seccomp +# profile set and is profile not set to "unconfined" +getSeccompContainers[container] { + some i + keys := [key | key := sprintf("%s/%s", [ + "container.seccomp.security.alpha.kubernetes.io", + kubernetes.containers[_].name, + ])] + seccomp := object.filter(kubernetes.annotations[_], keys) + val := seccomp[i] + val != "unconfined" + [a, c] := split(i, "/") + container = c } -failSeccomp[profile] { +# getNoSeccompContainers returns all containers which do not have +# a seccomp profile specified or profile set to "unconfined" +getNoSeccompContainers[container] { + container := kubernetes.containers[_].name + not getSeccompContainers[container] +} + +# getContainersWithDisallowedSeccompProfileType returns all containers which have a seccomp +# profile set and is profile set to "Unconfined" +getContainersWithDisallowedSeccompProfileType[name] { + container := kubernetes.containers[_] + type := container.securityContext.seccompProfile.type + type == "Unconfined" + name = container.name +} + +# getContainersWithDisallowedSeccompProfileType returns all containers which do not have +# a seccomp profile type specified +getContainersWithDisallowedSeccompProfileType[name] { container := kubernetes.containers[_] - profile := container.securityContext.seccompProfile - profile.type == "Unconfined" + not container.securityContext.seccompProfile.type + name = container.name } deny[res] { - cause := failSeccomp[_] - msg := "You should not set Seccomp profile to 'Unconfined'." + cause := getContainersWithDisallowedSeccompProfileType[_] + msg := kubernetes.format(sprintf("container %s of %s %s in %s namespace should specify a seccomp profile", [getNoSeccompContainers[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) res := result.new(msg, cause) } diff --git a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego index c26fd4f3a..9afdf4224 100644 --- a/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego +++ b/rules/kubernetes/policies/pss/baseline/11_seccomp_profile_unconfined_test.rego @@ -1,128 +1,42 @@ package builtin.kubernetes.KSV104 -test_base_securityContext_seccompProfile_unconfined_denied { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {"type": "Unconfined"}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 1 - r[_].msg == "You should not set Seccomp profile to 'Unconfined'." -} - -test_base_securityContext_seccompProfile_unspecified_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 0 -} - -test_base_securityContext_seccompProfile_RuntimeDefault_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": { - "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, - "containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - }], - }, - } - - count(r) == 0 -} - -test_container_securityContext_seccompProfile_unconfined_denied { +test_container_seccomp_profile_unconfined_denied { r := deny with input as { "apiVersion": "v1", "kind": "Pod", "metadata": {"name": "hello-sysctls"}, "spec": {"containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", "name": "hello", - "securityContext": {"seccompProfile": {"type": "Unconfined"}}, - }]}, - } - - count(r) == 1 - r[_].msg == "You should not set Seccomp profile to 'Unconfined'." -} - -test_container_securityContext_seccompProfile_unspecified_allowed { - r := deny with input as { - "apiVersion": "v1", - "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": {"containers": [{ + "image": "busybox", "command": [ "sh", "-c", "echo 'Hello' && sleep 1h", ], - "image": "busybox", - "name": "hello", - "securityContext": {"seccompProfile": {}}, + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, }]}, } count(r) == 0 } -test_container_securityContext_seccompProfile_RuntimeDefault_allowed { +test_container_seccomp_profile_unconfined_allowed { r := deny with input as { "apiVersion": "v1", "kind": "Pod", - "metadata": {"name": "hello-sysctls"}, - "spec": {"containers": [{ - "command": [ - "sh", - "-c", - "echo 'Hello' && sleep 1h", - ], - "image": "busybox", - "name": "hello", - "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, - }]}, + "metadata": {"name": "my-pod"}, + "spec": {"containers": [ + { + "name": "container-1", + "image": "nginx", + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + { + "name": "container-2", + "image": "busybox", + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + ]}, } count(r) == 0 diff --git a/rules/kubernetes/policies/pss/baseline/2_privileged.rego b/rules/kubernetes/policies/pss/baseline/2_privileged.rego index 99fea8dd4..84ea914ff 100644 --- a/rules/kubernetes/policies/pss/baseline/2_privileged.rego +++ b/rules/kubernetes/policies/pss/baseline/2_privileged.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Privileged container" +# title: "Privileged" # description: "Privileged containers share namespaces with the host system and do not offer any security. They should be used exclusively for system containers that require high privileges." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego b/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego index 6530cae8b..026897365 100644 --- a/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego +++ b/rules/kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Non-default capabilities added" +# title: "Specific capabilities added" # description: "Adding NET_RAW or capabilities beyond the default set must be disallowed." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego b/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego index 655ffb8d6..5b56e6cec 100644 --- a/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego +++ b/rules/kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default AppArmor profile not set" +# title: "Runtime/Default AppArmor profile not set" # description: "A program inside the container can bypass AppArmor protection policies." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego b/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego index 84f1e1a16..195c09cca 100644 --- a/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego +++ b/rules/kubernetes/policies/pss/restricted/1_non_core_volume_types.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Non-ephemeral volume types used" +# title: "Non-core volume types used." # description: "In addition to restricting HostPath volumes, usage of non-ephemeral volume types should be limited to those defined through PersistentVolumes." # scope: package # schemas: @@ -42,6 +42,7 @@ disallowed_volume_types = [ "portworxVolume", "scaleIO", "storageos", + "csi", ] # getDisallowedVolumes returns a list of volume names diff --git a/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego b/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego index a5d41509e..30ba9be21 100644 --- a/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego +++ b/rules/kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Process can elevate its own privileges" +# title: "Can elevate its own privileges" # description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node." # scope: package # schemas: diff --git a/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego b/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego index b44a1ad7c..f4ca8ba86 100644 --- a/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego +++ b/rules/kubernetes/policies/pss/restricted/5_runtime_default_seccomp_profile_not_set.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Default Seccomp profile not set" +# title: "Runtime/Default Seccomp profile not set" # description: "The RuntimeDefault/Localhost seccomp profile must be required, or allow specific additional profiles." # scope: package # schemas: diff --git a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego index 69a8459c3..592f8206c 100644 --- a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego +++ b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required.rego @@ -1,5 +1,5 @@ # METADATA -# title: "Ensure that the cluster-admin role is only used where required" +# title: "User with admin access" # description: "The RBAC role cluster-admin provides wide-ranging powers over the environment and should be used only where and when needed." # scope: package # schemas: @@ -19,16 +19,17 @@ package builtin.kubernetes.KSV111 import data.lib.kubernetes +readRoleRefs := ["cluster-admin", "admin", "edit"] + roleBindings := ["clusterrolebinding", "rolebinding"] clusterAdminRoleInUse(roleBinding) { lower(kubernetes.kind) == roleBindings[_] - roleBinding.roleRef.name == "cluster-admin" - roleBinding.subjects[_].name != "system:masters" + roleBinding.roleRef.name == readRoleRefs[_] } deny[res] { clusterAdminRoleInUse(input) - msg := sprintf("%s '%s' with role 'cluster-admin' should be used only when required", [kubernetes.kind, kubernetes.name]) + msg := kubernetes.format(sprintf("%s '%s' should not bind to roles %s", [kubernetes.kind, kubernetes.name, readRoleRefs])) res := result.new(msg, input.metadata) } diff --git a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego index 8c1e4a799..d33e5207c 100644 --- a/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego +++ b/rules/kubernetes/policies/rolebinding/cluster_admin_role_is_only_used_where_required_test.rego @@ -43,5 +43,51 @@ test_cluster_role_admin__used_with_system_role_binding { }, } + count(r) == 1 +} + +test_admin_used_with_system_role_binding { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "system:read-pods", + "namespace": "default", + }, + "subjects": [{ + "kind": "User", + "name": "system:masters", + "apiGroup": "rbac.authorization.k8s.io", + }], + "roleRef": { + "kind": "Role", + "name": "admin", + "apiGroup": "rbac.authorization.k8s.io", + }, + } + + count(r) == 1 +} + +test_non_role_ref_used_with_system_role_binding { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "system:read-pods", + "namespace": "default", + }, + "subjects": [{ + "kind": "User", + "name": "system:masters", + "apiGroup": "rbac.authorization.k8s.io", + }], + "roleRef": { + "kind": "Role", + "name": "admin1", + "apiGroup": "rbac.authorization.k8s.io", + }, + } + count(r) == 0 }