From 54afa1df69e12612e22ace783c0557b73534685a Mon Sep 17 00:00:00 2001 From: ArturRibeiro-CX <153724638+cx-artur-ribeiro@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:16:39 +0000 Subject: [PATCH] fix: update Missing AppArmor Profile query for new kubernetes syntax --- .../k8s/missing_app_armor_config/query.rego | 134 +++++++++++++++--- .../test/{negative.yaml => negative1.yaml} | 2 +- .../test/negative2.yaml | 26 ++++ .../test/{positive.yaml => positive1.yaml} | 0 .../test/positive2.yaml | 57 ++++++++ .../test/positive3.yaml | 24 ++++ .../test/positive_expected_result.json | 48 ++++++- 7 files changed, 270 insertions(+), 21 deletions(-) rename assets/queries/k8s/missing_app_armor_config/test/{negative.yaml => negative1.yaml} (99%) create mode 100644 assets/queries/k8s/missing_app_armor_config/test/negative2.yaml rename assets/queries/k8s/missing_app_armor_config/test/{positive.yaml => positive1.yaml} (100%) create mode 100644 assets/queries/k8s/missing_app_armor_config/test/positive2.yaml create mode 100644 assets/queries/k8s/missing_app_armor_config/test/positive3.yaml diff --git a/assets/queries/k8s/missing_app_armor_config/query.rego b/assets/queries/k8s/missing_app_armor_config/query.rego index 5a32a6b5c4a..94646434cbd 100644 --- a/assets/queries/k8s/missing_app_armor_config/query.rego +++ b/assets/queries/k8s/missing_app_armor_config/query.rego @@ -23,30 +23,63 @@ isValidAppArmorProfile(profile) { startswith(profile, "localhost/") } +# Valid types for the new securityContext.appArmorProfile API (Kubernetes 1.23+). +# "Unconfined" is not a valid type as stated in the documentation. +validAppArmorProfileType(t) { + t == "RuntimeDefault" +} else { + t == "Localhost" +} + +# True when the container has valid AppArmor via securityContext.appArmorProfile +hasValidAppArmorProfileNewSyntax(document, typeKey, containerIndex) { + specInfo := k8sLib.getSpecInfo(document) + container := specInfo.spec[typeKey][containerIndex] + containerAppArmor := object.get(object.get(container, "securityContext", {}), "appArmorProfile", {}) + containerAppArmor.type != null + validAppArmorProfileType(containerAppArmor.type) +} + +# True when the pod has valid AppArmor via securityContext.appArmorProfile +hasValidAppArmorProfileNewSyntax(document, typeKey, containerIndex) { + specInfo := k8sLib.getSpecInfo(document) + container := specInfo.spec[typeKey][containerIndex] + containerAppArmor := object.get(object.get(container, "securityContext", {}), "appArmorProfile", {}) + not containerAppArmor.type + podAppArmor := object.get(object.get(specInfo.spec, "securityContext", {}), "appArmorProfile", {}) + podAppArmor.type != null + validAppArmorProfileType(podAppArmor.type) +} + CxPolicy[result] { document := input.document[i] metadata := document.metadata specInfo := k8sLib.getSpecInfo(document) - container := specInfo.spec[types[x]][_].name + container := specInfo.spec[types[x]][c].name metadataInfo := getMetadataInfo(document) annotations := object.get(metadataInfo.metadata, "annotations", {}) expectedKey := sprintf("container.apparmor.security.beta.kubernetes.io/%s", [container]) not isValidAppArmorProfile(annotations[expectedKey]) + not hasValidAppArmorProfileNewSyntax(document, x, c) annotationsPath := trim_left(sprintf("%s.annotations", [metadataInfo.path]), ".") + searchPath := get_apparmor_search_path(specInfo, x, c, annotationsPath) + msg := get_apparmor_messages(searchPath, metadata.name, container, "IncorrectValue", expectedKey, annotationsPath) + searchValue := get_apparmor_search_value(searchPath, document.kind, expectedKey, x, c, "IncorrectValue") + result := { "documentId": document.id, "resourceType": document.kind, "resourceName": metadata.name, - "searchKey": trim_right(sprintf("metadata.name={{%s}}.%s", [metadata.name, metadataInfo.path]), "."), - "issueType": "IncorrectValue", - "searchValue": sprintf("%s%s", [document.kind, expectedKey]), # handle multiple kinds and key combinations - "keyExpectedValue": sprintf("metadata.name={{%s}}.%s[%s] should be set to 'runtime/default' or 'localhost'", [metadata.name, annotationsPath, expectedKey]), - "keyActualValue": sprintf("metadata.name={{%s}}.%s[%s] does not specify a valid AppArmor profile", [metadata.name, annotationsPath, expectedKey]), - "searchLine": search_line_metadata(annotationsPath), + "searchKey": sprintf("metadata.name={{%s}}.%s", [metadata.name, searchPath]), + "issueType": "IncorrectValue", + "searchValue": searchValue, + "keyExpectedValue": msg.keyExpectedValue, + "keyActualValue": msg.keyActualValue, + "searchLine": build_search_line_for_apparmor(searchPath), } } @@ -62,24 +95,93 @@ CxPolicy[result] { expectedKey := sprintf("container.apparmor.security.beta.kubernetes.io/%s", [container]) not common_lib.valid_key(annotations, expectedKey) + not hasValidAppArmorProfileNewSyntax(document, x, c) annotationsPath := trim_left(sprintf("%s.annotations", [metadataInfo.path]), ".") + searchPath := get_apparmor_search_path(specInfo, x, c, annotationsPath) + msg := get_apparmor_messages(searchPath, metadata.name, container, "MissingAttribute", expectedKey, annotationsPath) + searchValue := get_apparmor_search_value(searchPath, document.kind, expectedKey, x, c, "MissingAttribute") + result := { "documentId": document.id, "resourceType": document.kind, "resourceName": metadata.name, - "searchKey": sprintf("metadata.name={{%s}}.%s", [metadata.name, annotationsPath]), + "searchKey": sprintf("metadata.name={{%s}}.%s", [metadata.name, searchPath]), "issueType": "MissingAttribute", - "searchValue": sprintf("%s%s%d", [document.kind, types[x], c]), # handle multiple kinds and container combination - "keyExpectedValue": sprintf("metadata.name={{%s}}.%s should specify an AppArmor profile for container {{%s}}", [metadata.name, annotationsPath, container]), - "keyActualValue": sprintf("metadata.name={{%s}}.%s does not specify an AppArmor profile for container {{%s}}", [metadata.name, annotationsPath, container]), - "searchLine": search_line_metadata(annotationsPath), + "searchValue": searchValue, + "keyExpectedValue": msg.keyExpectedValue, + "keyActualValue": msg.keyActualValue, + "searchLine": build_search_line_for_apparmor(searchPath), } } -search_line_metadata(annotationsPath) = searchLine { - annotationsPath == "annotations" - searchLine := common_lib.build_search_line(["metadata", "annotations"], []) +# searchValue for similarity ID / result identity: use path when new syntax, else annotation-style value. +get_apparmor_search_value(searchPath, kind, expectedKey, typeKey, containerIndex, issueType) = v { + contains(searchPath, "securityContext") + v := sprintf("%s.%s", [kind, searchPath]) +} else = v { + issueType == "IncorrectValue" + v := sprintf("%s%s", [kind, expectedKey]) +} else = v { + v := sprintf("%s%s%d", [kind, typeKey, containerIndex]) +} + +# Returns keyExpectedValue and keyActualValue aligned with the path we point to (annotations vs securityContext). +get_apparmor_messages(searchPath, metadataName, container, issueType, expectedKey, annotationsPath) = msg { + contains(searchPath, "securityContext") + issueType == "MissingAttribute" + msg := { + "keyExpectedValue": sprintf("metadata.name={{%s}}.%s should specify an AppArmor profile (e.g. appArmorProfile.type: RuntimeDefault or Localhost) for container {{%s}}", [metadataName, searchPath, container]), + "keyActualValue": sprintf("metadata.name={{%s}}.%s does not specify an AppArmor profile for container {{%s}}", [metadataName, searchPath, container]), + } +} else = msg { + contains(searchPath, "securityContext") + issueType == "IncorrectValue" + msg := { + "keyExpectedValue": sprintf("metadata.name={{%s}}.%s.type should be set to 'RuntimeDefault' or 'Localhost'", [metadataName, searchPath]), + "keyActualValue": sprintf("metadata.name={{%s}}.%s.type does not specify a valid AppArmor profile (Unconfined is not accepted)", [metadataName, searchPath]), + } +} else = msg { + issueType == "MissingAttribute" + msg := { + "keyExpectedValue": sprintf("metadata.name={{%s}}.%s should specify an AppArmor profile for container {{%s}}", [metadataName, annotationsPath, container]), + "keyActualValue": sprintf("metadata.name={{%s}}.%s does not specify an AppArmor profile for container {{%s}}", [metadataName, annotationsPath, container]), + } +} else = msg { + msg := { + "keyExpectedValue": sprintf("metadata.name={{%s}}.%s[%s] should be set to 'runtime/default' or 'localhost'", [metadataName, annotationsPath, expectedKey]), + "keyActualValue": sprintf("metadata.name={{%s}}.%s[%s] does not specify a valid AppArmor profile", [metadataName, annotationsPath, expectedKey]), + } +} + +# build search path for apparmor profile +get_apparmor_search_path(specInfo, typeKey, containerIndex, annotationsPath) = path { + container := specInfo.spec[typeKey][containerIndex] + common_lib.valid_key(container, "securityContext") + common_lib.valid_key(object.get(container, "securityContext", {}), "appArmorProfile") + path := sprintf("%s.%s.%d.securityContext.appArmorProfile", [specInfo.path, typeKey, containerIndex]) +} else = path { + container := specInfo.spec[typeKey][containerIndex] + common_lib.valid_key(container, "securityContext") + path := sprintf("%s.%s.%d.securityContext", [specInfo.path, typeKey, containerIndex]) +} else = path { + common_lib.valid_key(specInfo.spec, "securityContext") + common_lib.valid_key(object.get(specInfo.spec, "securityContext", {}), "appArmorProfile") + path := sprintf("%s.securityContext.appArmorProfile", [specInfo.path]) +} else = path { + common_lib.valid_key(specInfo.spec, "securityContext") + path := sprintf("%s.securityContext", [specInfo.path]) +} else = path { + path := annotationsPath +} + +# Builds search line for either annotation path (old syntax) or spec path (new syntax). +build_search_line_for_apparmor(path) = searchLine { + path == "annotations" + searchLine := common_lib.build_search_line(["metadata", "annotations"], []) +} else = searchLine { + contains(path, "securityContext") + searchLine := common_lib.build_search_line(split(path, "."), []) } else = searchLine { - searchLine := common_lib.build_search_line(split(annotationsPath, "."), []) + searchLine := common_lib.build_search_line(split(path, "."), []) } diff --git a/assets/queries/k8s/missing_app_armor_config/test/negative.yaml b/assets/queries/k8s/missing_app_armor_config/test/negative1.yaml similarity index 99% rename from assets/queries/k8s/missing_app_armor_config/test/negative.yaml rename to assets/queries/k8s/missing_app_armor_config/test/negative1.yaml index f65c0bf8983..5d1fd69338f 100644 --- a/assets/queries/k8s/missing_app_armor_config/test/negative.yaml +++ b/assets/queries/k8s/missing_app_armor_config/test/negative1.yaml @@ -8,4 +8,4 @@ spec: containers: - name: hello image: busybox - command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] \ No newline at end of file + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] diff --git a/assets/queries/k8s/missing_app_armor_config/test/negative2.yaml b/assets/queries/k8s/missing_app_armor_config/test/negative2.yaml new file mode 100644 index 00000000000..d2a16eb3e40 --- /dev/null +++ b/assets/queries/k8s/missing_app_armor_config/test/negative2.yaml @@ -0,0 +1,26 @@ +# New Kubernetes syntax: AppArmor via securityContext.appArmorProfile (no annotations) +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-runtime-default +spec: + securityContext: + appArmorProfile: + type: RuntimeDefault + containers: + - name: hello + image: busybox + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-runtime-default2 +spec: + securityContext: + appArmorProfile: + type: Localhost + containers: + - name: hello + image: busybox + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] \ No newline at end of file diff --git a/assets/queries/k8s/missing_app_armor_config/test/positive.yaml b/assets/queries/k8s/missing_app_armor_config/test/positive1.yaml similarity index 100% rename from assets/queries/k8s/missing_app_armor_config/test/positive.yaml rename to assets/queries/k8s/missing_app_armor_config/test/positive1.yaml diff --git a/assets/queries/k8s/missing_app_armor_config/test/positive2.yaml b/assets/queries/k8s/missing_app_armor_config/test/positive2.yaml new file mode 100644 index 00000000000..eeaf691c525 --- /dev/null +++ b/assets/queries/k8s/missing_app_armor_config/test/positive2.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-no-profile +spec: + securityContext: + runAsNonRoot: true + containers: + - name: hello + image: busybox + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-empty-seccontext +spec: + securityContext: {} + containers: + - name: hello + image: busybox + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-unconfined +spec: + securityContext: + appArmorProfile: + type: Unconfined + containers: + - name: hello + image: busybox + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ubuntu-apparmor-no-profile + namespace: testns +spec: + replicas: 1 + selector: + matchLabels: + app: ubuntu + template: + metadata: + labels: + app: ubuntu + spec: + securityContext: + runAsNonRoot: true + containers: + - name: ubuntu-container + image: busybox + command: [ "sh", "-c", "sleep 1h" ] diff --git a/assets/queries/k8s/missing_app_armor_config/test/positive3.yaml b/assets/queries/k8s/missing_app_armor_config/test/positive3.yaml new file mode 100644 index 00000000000..ad110f09b6e --- /dev/null +++ b/assets/queries/k8s/missing_app_armor_config/test/positive3.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-container-seccontext +spec: + containers: + - name: hello + image: busybox + securityContext: + runAsNonRoot: true + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello-apparmor-container-unconfined +spec: + containers: + - name: hello + image: busybox + securityContext: + appArmorProfile: + type: Unconfined + command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ] diff --git a/assets/queries/k8s/missing_app_armor_config/test/positive_expected_result.json b/assets/queries/k8s/missing_app_armor_config/test/positive_expected_result.json index 9750d762fc9..ab4721095fe 100644 --- a/assets/queries/k8s/missing_app_armor_config/test/positive_expected_result.json +++ b/assets/queries/k8s/missing_app_armor_config/test/positive_expected_result.json @@ -2,21 +2,61 @@ { "queryName": "Missing AppArmor Profile", "severity": "LOW", - "line": 5 + "line": 5, + "fileName": "positive1.yaml" }, { "queryName": "Missing AppArmor Profile", "severity": "LOW", - "line": 5 + "line": 5, + "fileName": "positive1.yaml" }, { "queryName": "Missing AppArmor Profile", "severity": "LOW", - "line": 5 + "line": 5, + "fileName": "positive1.yaml" }, { "queryName": "Missing AppArmor Profile", "severity": "LOW", - "line": 36 + "line": 36, + "fileName": "positive1.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 6, + "fileName": "positive2.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 18, + "fileName": "positive2.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 30, + "fileName": "positive2.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 52, + "fileName": "positive2.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 9, + "fileName": "positive3.yaml" + }, + { + "queryName": "Missing AppArmor Profile", + "severity": "LOW", + "line": 22, + "fileName": "positive3.yaml" } ]