Repository: controlplaneio/kubesec

Issue:
appArmor in securityContext
It seems kubesec checks the appArmor configuration from the Pod's annotation. As appArmor is now stable in SecurityContext, will this check be modified in a next version ?
Thanks for this great project 👍

Discussion:
@Santosh1176: Hi,
Can I work on this?@sublimino: @Santosh1176 yes we'd welcome a PR, thanks for the report @lucj! @esticansat: Seems like this issue hasn't seen much movement for 6 months, so I'll be working on this one now.

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 5e644c3..cbcaf4a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@ RUN set -x && \
     curl -sSL --output-dir "${SCHEMA_PATH}" -O "${BASE_URL}/${SCHEMA_PATH}/deployment-apps-v1.json" && \
     curl -sSL --output-dir "${SCHEMA_PATH}" -O "${BASE_URL}/${SCHEMA_PATH}/statefulset-apps-v1.json"
 
-FROM golang:1.24 AS builder
+FROM golang:1.25 AS builder
 
 WORKDIR /kubesec
 
diff --git a/Dockerfile.scratch b/Dockerfile.scratch
index 31b126c..ccf94a3 100644
--- a/Dockerfile.scratch
+++ b/Dockerfile.scratch
@@ -15,7 +15,7 @@ RUN set -x && \
     curl -sSL --output-dir "${SCHEMA_PATH}" -O "${BASE_URL}/${SCHEMA_PATH}/deployment-apps-v1.json" && \
     curl -sSL --output-dir "${SCHEMA_PATH}" -O "${BASE_URL}/${SCHEMA_PATH}/statefulset-apps-v1.json"
 
-FROM golang:1.24-alpine AS builder
+FROM golang:1.25-alpine AS builder
 
 RUN echo "kubesec:x:31012:31012:kubesec:/home/kubesec:/sbin/nologin" > /passwd && \
   echo "kubesec:x:31012:" > /group
diff --git a/pkg/ruler/ruleset.go b/pkg/ruler/ruleset.go
index 727ae8b..f30daf3 100644
--- a/pkg/ruler/ruleset.go
+++ b/pkg/ruler/ruleset.go
@@ -250,13 +250,23 @@ func NewRuleset(logger *zap.SugaredLogger) *Ruleset {
 	apparmorAnyRule := Rule{
 		Predicate: rules.ApparmorAny,
 		ID:        "ApparmorAny",
-		Selector:  ".metadata .annotations .\"container.apparmor.security.beta.kubernetes.io/nginx\"",
-		Reason:    "Well defined AppArmor policies may provide greater protection from unknown threats. WARNING: NOT PRODUCTION READY",
+		Selector:  ".spec .securityContext .appArmorProfile .type | .spec .containers[] .securityContext .appArmorProfile .type | .spec .initContainers[] .securityContext .appArmorProfile .type | .spec .ephemeralContainers[] .securityContext .appArmorProfile .type",
+		Reason:    "Well defined AppArmor policies may provide greater protection from unknown threats.",
 		Kinds:     []string{"Pod", "Deployment", "StatefulSet", "DaemonSet"},
 		Points:    3,
 	}
 	list = append(list, apparmorAnyRule)
 
+	apparmorUnconfinedRule := Rule{
+		Predicate: rules.ApparmorUnconfined,
+		ID:        "ApparmorUnconfined",
+		Selector:  ".spec .securityContext .appArmorProfile .type | .spec .containers[] .securityContext .appArmorProfile .type | .spec .initContainers[] .securityContext .appArmorProfile .type | .spec .ephemeralContainers[] .securityContext .appArmorProfile .type",
+		Reason:    "Unconfined AppArmor profiles disable AppArmor enforcement on the workloads",
+		Kinds:     []string{"Pod", "Deployment", "StatefulSet", "DaemonSet"},
+		Points:    -1,
+	}
+	list = append(list, apparmorUnconfinedRule)
+
 	volumeClaimAccessModeReadWriteOnce := Rule{
 		Predicate: rules.VolumeClaimAccessModeReadWriteOnce,
 		ID:        "VolumeClaimAccessModeReadWriteOnce",
diff --git a/pkg/rules/apparmorAny.go b/pkg/rules/apparmorAny.go
index 9c99014..3dabccf 100644
--- a/pkg/rules/apparmorAny.go
+++ b/pkg/rules/apparmorAny.go
@@ -1,42 +1,33 @@
 package rules
 
-import (
-	"bytes"
-	"fmt"
-	"github.com/thedevsaddam/gojsonq/v2"
-	"regexp"
-	"strings"
-)
+import "github.com/thedevsaddam/gojsonq/v2"
 
-// TODO(ajm): tighten these matches, they could be "[apparmor..." or " apparmor...", and "unconfined]" or "unconfined "
-// TODO(ajm): space delimiting matches is insufficient as this could be set to `unconfined blah`
-func ApparmorAny(json []byte) int {
-	containers := 0
-	startWordBoundaryRegex := "[\\[ ]"
-	endWordBoundaryRegex := "[\\] ]"
-
-	annotations := gojsonq.New().Reader(bytes.NewReader(json)).
-		From("metadata.annotations").Get()
+// isApparmorUnconfined checks the appArmorProfile.type field and returns a checkSecurityContextResult struct.
+// If the field is set then unset=false. If, on top of that, the value of Unconfined matches the expected value
+// then return valid=true.
+func isApparmorUnconfined(jq *gojsonq.JSONQ, expectedUnconfined bool) checkSecurityContextResult {
+	value := jq.From("securityContext.appArmorProfile.type").Get()
 
-	annotationsString := fmt.Sprintf("%v", annotations)
+	v, ok := value.(string)
 
-	if annotations != nil && strings.Contains(annotationsString, "container.apparmor.security.beta.kubernetes.io/pod:") {
-		if !strings.Contains(annotationsString, "container.apparmor.security.beta.kubernetes.io/pod:unconfined") {
-			containers++
-		}
-	} else if annotations != nil {
+	res := checkSecurityContextResult{}
 
-		keyNameRegex := "container\\.apparmor\\.security\\.beta\\.kubernetes\\.io/[a-zA-Z-.]+"
-		// TODO(ajm) match end of string in regex
-		isNamedPodMatch, _ := regexp.MatchString(startWordBoundaryRegex+keyNameRegex+":", annotationsString)
+	if !ok {
+		res.unset = true
+		return res
+	}
 
-		if isNamedPodMatch {
-			isUnconfinedNamedPodMatch, _ := regexp.MatchString(startWordBoundaryRegex+keyNameRegex+":unconfined"+endWordBoundaryRegex, annotationsString)
-			if !isUnconfinedNamedPodMatch {
-				containers++
-			}
-		}
+	return checkSecurityContextResult{
+		valid: (v == "Unconfined") == expectedUnconfined,
 	}
+}
 
-	return containers
+func ApparmorAny(json []byte) int {
+	return checkSecurityContext(
+		json,
+		true, // present in Pod Security Context
+		func(jq *gojsonq.JSONQ) checkSecurityContextResult {
+			return isApparmorUnconfined(jq, false)
+		},
+	)
 }
diff --git a/pkg/rules/apparmorTestCases.go b/pkg/rules/apparmorTestCases.go
new file mode 100644
index 0000000..77b22db
--- /dev/null
+++ b/pkg/rules/apparmorTestCases.go
@@ -0,0 +1,250 @@
+package rules
+
+// Currently, appArmorProfile can appear under the following places (search for 'AppArmor' in
+// the PSS docs https://kubernetes.io/docs/concepts/security/pod-security-standards/):
+//
+// 	spec.securityContext.appArmorProfile.type
+// 	spec.containers[*].securityContext.appArmorProfile.type
+// 	spec.initContainers[*].securityContext.appArmorProfile.type
+// 	spec.ephemeralContainers[*].securityContext.appArmorProfile
+
+const (
+	tcmanAppArmorProfileMissing = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  containers:
+    - name: trustworthy-container
+      image: sotrustworthy:latest
+`
+	tcmanAppArmorProfileInSecCtxRD = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  securityContext:
+    appArmorProfile:
+      type: RuntimeDefault
+  containers:
+    - name: trustworthy-container
+      image: sotrustworthy:latest
+`
+	tcmanAppArmorProfileInSecCtxUn = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  securityContext:
+    appArmorProfile:
+      type: Unconfined
+  containers:
+    - name: trustworthy-container
+      image: sotrustworthy:latest
+`
+	tcmanAppArmorProfileInSecCtxLH = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  securityContext:
+    appArmorProfile:
+      type: LocalHost
+      localhostProfile: profiles/audit.json
+  containers:
+    - name: trustworthy-container
+      image: sotrustworthy:latest
+`
+	tcmanAppArmorProfileInContainerRD = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  containers:
+  - name: trustworthy-container
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: RuntimeDefault
+`
+	tcmanAppArmorProfileInContainerUn = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  containers:
+  - name: trustworthy-container
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: Unconfined
+`
+	tcmanAppArmorProfileInContainerLH = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  containers:
+  - name: trustworthy-container
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: LocalHost
+        localhostProfile: profiles/audit.json
+`
+	tcmanAppArmorProfileInInitRD = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  initContainers:
+  - name: trustworthy-initcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: RuntimeDefault
+`
+	tcmanAppArmorProfileInInitUn = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  initContainers:
+  - name: trustworthy-initcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: Unconfined
+`
+	tcmanAppArmorProfileInInitLH = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  initContainers:
+  - name: trustworthy-initcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: LocalHost
+        localhostProfile: profiles/audit.json
+`
+	tcmanAppArmorProfileInEphRD = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  ephemeralContainers:
+  - name: trustworthy-ephcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: RuntimeDefault
+`
+	tcmanAppArmorProfileInEphUn = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  ephemeralContainers:
+  - name: trustworthy-ephcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: Unconfined
+`
+	tcmanAppArmorProfileInEphLH = `
+---
+apiVersion: v1
+kind: Pod
+spec:
+  ephemeralContainers:
+  - name: trustworthy-ephcontainer
+    image: sotrustworthy:latest
+    securityContext:
+      appArmorProfile:
+        type: LocalHost
+        localhostProfile: profiles/audit.json
+`
+)
+
+type tcAppArmorProfileType string
+
+const (
+	tcprofAppArmorProfileMissing tcAppArmorProfileType = ""
+	tcprofAppArmorUnconfined     tcAppArmorProfileType = "Unconfined"
+	tcprofAppArmorRuntimeDefault tcAppArmorProfileType = "RuntimeDefault"
+	tcprofAppArmorLocalhost      tcAppArmorProfileType = "LocalHost"
+)
+
+var testCasesApparmor = []struct {
+	description         string
+	expectedProfileType tcAppArmorProfileType
+	manifest            string
+}{
+	{
+		description:         "Missing appArmorProfile",
+		expectedProfileType: tcprofAppArmorProfileMissing,
+		manifest:            tcmanAppArmorProfileMissing,
+	},
+	{
+		description:         "RuntimeDefault appArmorProfile found inside .spec.securityContext",
+		expectedProfileType: tcprofAppArmorRuntimeDefault,
+		manifest:            tcmanAppArmorProfileInSecCtxRD,
+	},
+	{
+		description:         "Unconfined appArmorProfile found inside .spec.securityContext",
+		expectedProfileType: tcprofAppArmorUnconfined,
+		manifest:            tcmanAppArmorProfileInSecCtxUn,
+	},
+	{
+		description:         "LocalHost appArmorProfile found inside .spec.securityContext",
+		expectedProfileType: tcprofAppArmorLocalhost,
+		manifest:            tcmanAppArmorProfileInSecCtxLH,
+	},
+	{
+		description:         "RuntimeDefault appArmorProfile found inside .spec.containers[*].securityContext",
+		expectedProfileType: tcprofAppArmorRuntimeDefault,
+		manifest:            tcmanAppArmorProfileInContainerRD,
+	},
+	{
+		description:         "Unconfined appArmorProfile found inside .spec.containers[*].securityContext",
+		expectedProfileType: tcprofAppArmorUnconfined,
+		manifest:            tcmanAppArmorProfileInContainerUn,
+	},
+	{
+		description:         "LocalHost appArmorProfile found inside .spec.containers[*].securityContext",
+		expectedProfileType: tcprofAppArmorLocalhost,
+		manifest:            tcmanAppArmorProfileInContainerLH,
+	},
+	{
+		description:         "RuntimeDefault appArmorProfile found inside .spec.initContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorRuntimeDefault,
+		manifest:            tcmanAppArmorProfileInInitRD,
+	},
+	{
+		description:         "Unconfined appArmorProfile found inside .spec.initContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorUnconfined,
+		manifest:            tcmanAppArmorProfileInInitUn,
+	},
+	{
+		description:         "LocalHost appArmorProfile found inside .spec.initContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorLocalhost,
+		manifest:            tcmanAppArmorProfileInInitLH,
+	},
+	{
+		description:         "RuntimeDefault appArmorProfile found inside .spec.ephemeralContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorRuntimeDefault,
+		manifest:            tcmanAppArmorProfileInEphRD,
+	},
+	{
+		description:         "Unconfined appArmorProfile found inside .spec.ephemeralContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorUnconfined,
+		manifest:            tcmanAppArmorProfileInEphUn,
+	},
+	{
+		description:         "LocalHost appArmorProfile found inside .spec.ephemeralContainers[*].securityContext",
+		expectedProfileType: tcprofAppArmorLocalhost,
+		manifest:            tcmanAppArmorProfileInEphLH,
+	},
+}
diff --git a/pkg/rules/apparmorUnconfined.go b/pkg/rules/apparmorUnconfined.go
new file mode 100644
index 0000000..d1683ef
--- /dev/null
+++ b/pkg/rules/apparmorUnconfined.go
@@ -0,0 +1,13 @@
+package rules
+
+import "github.com/thedevsaddam/gojsonq/v2"
+
+func ApparmorUnconfined(json []byte) int {
+	return checkSecurityContext(
+		json,
+		true, // can be found in Pod Security Context
+		func(jq *gojsonq.JSONQ) checkSecurityContextResult {
+			return isApparmorUnconfined(jq, true)
+		},
+	)
+}



Test patch:
diff --git a/pkg/rules/apparmorAny_test.go b/pkg/rules/apparmorAny_test.go
index 9752b4b..b91cc8e 100644
--- a/pkg/rules/apparmorAny_test.go
+++ b/pkg/rules/apparmorAny_test.go
@@ -1,223 +1,110 @@
 package rules
 
 import (
-	"github.com/ghodss/yaml"
 	"testing"
-)
-
-func Test_ApparmorAny_Pod(t *testing.T) {
-	var data = `
----
-# The example Pod utilizing the profile loaded by the sample daemon.
-
-apiVersion: v1
-kind: Pod
-metadata:
-  name: nginx-apparmor
-  # Note that the Pod does not need to be in the same namespace as the loader.
-  labels:
-    app: nginx
-  annotations:
-    # Tell Kubernetes to apply the AppArmor profile "k8s-nginx".
-    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
-    container.apparmor.security.beta.kubernetes.io/nginx: localhost/k8s-nginx
-spec:
-  containers:
-  - name: nginx
-    image: nginx
-    ports:
-    - containerPort: 80
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
-
-	containers := ApparmorAny(json)
-	if containers != 1 {
-		t.Errorf("Got %v containers wanted %v", containers, 1)
-	}
-}
-
-func Test_ApparmorAny_Pod_Unconfined(t *testing.T) {
-	var data = `
----
-# The example Pod utilizing the profile loaded by the sample daemon.
-
-apiVersion: v1
-kind: Pod
-metadata:
-  name: nginx-apparmor
-  # Note that the Pod does not need to be in the same namespace as the loader.
-  labels:
-    app: nginx
-  annotations:
-    # Tell Kubernetes to apply the AppArmor profile "k8s-nginx".
-    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
-    container.apparmor.security.beta.kubernetes.io/nginx: unconfined
-spec:
-  containers:
-  - name: nginx
-    image: nginx
-    ports:
-    - containerPort: 80
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
-
-	containers := ApparmorAny(json)
-	if containers != 0 {
-		t.Errorf("Got %v containers wanted %v", containers, 0)
-	}
-}
-
-func Test_ApparmorAny_No_Seccomp(t *testing.T) {
-	var data = `
----
-# The example Pod utilizing the profile loaded by the sample daemon.
-
-apiVersion: v1
-kind: Pod
-metadata:
-  name: nginx-apparmor
-  # Note that the Pod does not need to be in the same namespace as the loader.
-  labels:
-    app: nginx
-  annotations:
-    # Tell Kubernetes to apply the AppArmor profile "k8s-nginx".
-    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
-    container.apparmor.security.beta.kubernetes.io/nginx: unconfined
-spec:
-  containers:
-  - name: nginx
-    image: nginx
-    ports:
-    - containerPort: 80
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
-
-	containers := ApparmorAny(json)
-	if containers != 0 {
-		t.Errorf("Got %v containers wanted %v", containers, 0)
-	}
-}
-
-func Test_ApparmorAny_Named_Pod(t *testing.T) {
-	var data = `
----
-# The example Pod utilizing the profile loaded by the sample daemon.
-
-apiVersion: v1
-kind: Pod
-metadata:
-  name: nginx-apparmor
-  # Note that the Pod does not need to be in the same namespace as the loader.
-  labels:
-    app: nginx
-  annotations:
-    # Tell Kubernetes to apply the AppArmor profile "k8s-nginx".
-    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
-    container.apparmor.security.beta.kubernetes.io/somePodName: localhost/k8s-nginx
-spec:
-  containers:
-  - name: nginx
-    image: nginx
-    ports:
-    - containerPort: 80
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
 
-	containers := ApparmorAny(json)
-	if containers != 1 {
-		t.Errorf("Got %v containers wanted %v", containers, 1)
-	}
-}
-
-func Test_ApparmorAny_Named_Pod_Special_Chars(t *testing.T) {
-	var data = `
----
-apiVersion: v1
-kind: Pod
-metadata:
-  annotations:
-    container.apparmor.security.beta.kubernetes.io/my-Named.Pod: runtime/default
-spec:
-containers:
-  - name: trustworthy-container
-    image: sotrustworthy:latest
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
-
-	containers := ApparmorAny(json)
-	if containers != 1 {
-		t.Errorf("Got %v containers wanted %v", containers, 1)
-	}
-}
-
-func Test_ApparmorAny_Named_Pod_Special_Chars_Unconfined(t *testing.T) {
-	var data = `
----
-apiVersion: v1
-kind: Pod
-metadata:
-annotations:
-  container.apparmor.security.beta.kubernetes.io/my-Named.Pod: unconfined
-spec:
-containers:
-  - name: trustworthy-container
-    image: sotrustworthy:latest
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
-	}
+	"github.com/ghodss/yaml"
+	"github.com/thedevsaddam/gojsonq/v2"
+)
 
-	containers := ApparmorAny(json)
-	if containers != 0 {
-		t.Errorf("Got %v containers wanted %v", containers, 0)
+func Test_ApparmorAny(t *testing.T) {
+	for _, tc := range testCasesApparmor {
+		t.Run(tc.description, func(t *testing.T) {
+			json, err := yaml.YAMLToJSON([]byte(tc.manifest))
+			if err != nil {
+				t.Fatal(err.Error())
+			}
+
+			count := ApparmorAny(json)
+			expectedCount := 0
+			if tc.expectedProfileType == tcprofAppArmorRuntimeDefault || tc.expectedProfileType == tcprofAppArmorLocalhost {
+				expectedCount = 1
+			}
+
+			if count != expectedCount {
+				t.Errorf("Expected count was %v but received %v", expectedCount, count)
+			}
+		})
 	}
 }
 
-func Test_ApparmorAny_Named_Pod_Illegal_Name(t *testing.T) {
-	var data = `
----
-apiVersion: v1
-kind: Pod
-metadata:
-annotations:
-  container.apparmor.security.beta.kubernetes.io/my-Named.Pod(illegal name): runtime/default
-spec:
-containers:
-  - name: trustworthy-container
-    image: sotrustworthy:latest
-`
-
-	json, err := yaml.YAMLToJSON([]byte(data))
-	if err != nil {
-		t.Fatal(err.Error())
+func Test_isApparmorUnconfined(t *testing.T) {
+	testCases := []struct {
+		description        string
+		json               string
+		expectedUnconfined bool
+		expectedResult     *checkSecurityContextResult
+	}{
+		{
+			description:        "field missing",
+			json:               `{}`,
+			expectedUnconfined: false,
+			expectedResult: &checkSecurityContextResult{
+				unset: true,
+				valid: false,
+			},
+		},
+		{
+			description:        "non-string field",
+			json:               `{"securityContext":{"appArmorProfile":{"type":123}}}`,
+			expectedUnconfined: false,
+			expectedResult: &checkSecurityContextResult{
+				unset: true,
+				valid: false,
+			},
+		},
+		{
+			description:        "Unconfined when expectedUnconfined=true",
+			json:               `{"securityContext":{"appArmorProfile":{"type":"Unconfined"}}}`,
+			expectedUnconfined: true,
+			expectedResult: &checkSecurityContextResult{
+				unset: false,
+				valid: true,
+			},
+		},
+		{
+			description:        "Unconfined when expectedUnconfined=false",
+			json:               `{"securityContext":{"appArmorProfile":{"type":"Unconfined"}}}`,
+			expectedUnconfined: false,
+			expectedResult: &checkSecurityContextResult{
+				unset: false,
+				valid: false,
+			},
+		},
+		{
+			description:        "Profile=RuntimeDefault when expectedUnconfined=false",
+			json:               `{"securityContext":{"appArmorProfile":{"type":"RuntimeDefault"}}}`,
+			expectedUnconfined: false,
+			expectedResult: &checkSecurityContextResult{
+				unset: false,
+				valid: true,
+			},
+		},
+		{
+			description:        "Profile=RuntimeDefault when expectedUnconfined=true",
+			json:               `{"securityContext":{"appArmorProfile":{"type":"RuntimeDefault"}}}`,
+			expectedUnconfined: true,
+			expectedResult: &checkSecurityContextResult{
+				unset: false,
+				valid: false,
+			},
+		},
 	}
 
-	containers := ApparmorAny(json)
-	if containers != 0 {
-		t.Errorf("Got %v containers wanted %v", containers, 0)
+	for _, tc := range testCases {
+		t.Run(tc.description, func(t *testing.T) {
+			jq := gojsonq.New().FromString(tc.json)
+			result := isApparmorUnconfined(jq, tc.expectedUnconfined)
+
+			if tc.expectedResult.unset != result.unset {
+				t.Errorf("expected 'checkSecurityContextResult.unset' value for test was %v but received %v instead",
+					tc.expectedResult.unset, result.unset)
+			}
+
+			if tc.expectedResult.valid != result.valid {
+				t.Errorf("expected 'checkSecurityContextResult.valid' value for test was %v but received %v instead",
+					tc.expectedResult.valid, result.valid)
+			}
+		})
 	}
 }
-
-// TODO(ajm) more apparmor tests for deployments
diff --git a/pkg/rules/apparmorUnconfined_test.go b/pkg/rules/apparmorUnconfined_test.go
new file mode 100644
index 0000000..00071ec
--- /dev/null
+++ b/pkg/rules/apparmorUnconfined_test.go
@@ -0,0 +1,28 @@
+package rules
+
+import (
+	"testing"
+
+	"github.com/ghodss/yaml"
+)
+
+func Test_apparmorUnconfined(t *testing.T) {
+	for _, tc := range testCasesApparmor {
+		t.Run(tc.description, func(t *testing.T) {
+			json, err := yaml.YAMLToJSON([]byte(tc.manifest))
+			if err != nil {
+				t.Fatal(err.Error())
+			}
+
+			count := ApparmorUnconfined(json)
+			expectedCount := 0
+			if tc.expectedProfileType == tcprofAppArmorUnconfined {
+				expectedCount = 1
+			}
+
+			if count != expectedCount {
+				t.Errorf("Expected count was %v but received %v", expectedCount, count)
+			}
+		})
+	}
+}
diff --git a/test/1_cli.bats b/test/1_cli.bats
index bc19eab..253d32b 100644
--- a/test/1_cli.bats
+++ b/test/1_cli.bats
@@ -133,11 +133,51 @@ teardown() {
   assert_lt_zero_points
 }
 
-@test "passes Pod with apparmor annotation" {
-  run _app "${TEST_DIR}/asset/score-3-pod-apparmor.yaml"
+@test "fails Deployment with unconfined apparmor for all containers" {
+  run _app "${TEST_DIR}/asset/score-0-dep-apparmor-empty-securitycontext.yml"
+  assert_zero_points
+}
+
+@test "passes Deployment with non-unconfined apparmor on the .spec section" {
+  run _app "${TEST_DIR}/asset/score-1-dep-apparmor-nonunconfined-spec-securitycontext.yml"
+  assert_gt_zero_points
+}
+
+@test "fails Deployment with unconfined apparmor on the .spec section" {
+  run _app "${TEST_DIR}/asset/score-0-dep-apparmor-unconfined-spec-securitycontext.yml"
+  assert_lt_zero_points
+}
+
+@test "passes Deployment with non-unconfined apparmor on the .spec.containers section" {
+  run _app "${TEST_DIR}/asset/score-1-dep-apparmor-nonunconfined-container.yml"
+  assert_gt_zero_points
+}
+
+@test "fails Deployment with unconfined apparmor on the .spec.containers section" {
+  run _app "${TEST_DIR}/asset/score-0-dep-apparmor-unconfined-container.yml"
+  assert_lt_zero_points
+}
+
+@test "passes Deployment with non-unconfined apparmor on the .spec.initContainers section" {
+  run _app "${TEST_DIR}/asset/score-1-dep-apparmor-nonunconfined-initcontainer.yml"
+  assert_gt_zero_points
+}
+
+@test "fails Deployment with unconfined apparmor on the .spec.initcontainers section" {
+  run _app "${TEST_DIR}/asset/score-0-dep-apparmor-unconfined-initcontainer.yml"
+  assert_lt_zero_points
+}
+
+@test "passes Deployment with non-unconfined apparmor on the .spec.ephemeralContainers section" {
+  run _app "${TEST_DIR}/asset/score-1-dep-apparmor-nonunconfined-ephemeralcontainer.yml"
   assert_gt_zero_points
 }
 
+@test "fails Deployment with unconfined apparmor on the .spec.ephemeralContainers section" {
+  run _app "${TEST_DIR}/asset/score-0-dep-apparmor-unconfined-ephemeralcontainer.yml"
+  assert_lt_zero_points
+}
+
 @test "fails Deployment with unconfined seccomp for all containers" {
   run _app "${TEST_DIR}/asset/score-0-dep-seccomp-empty-securitycontext.yml"
   assert_zero_points
diff --git a/test/2_regression.bats b/test/2_regression.bats
index 4c9a4f8..ea3e47f 100644
--- a/test/2_regression.bats
+++ b/test/2_regression.bats
@@ -183,7 +183,7 @@ teardown() {
   skip_if_not_remote
 
   run _app "${TEST_DIR}/asset/form-prefix-file.yml"
-  assert_gt_zero_points
+  assert_zero_points
   assert_success
 }
 
@@ -191,7 +191,7 @@ teardown() {
   skip_if_not_remote
 
   run _app "${TEST_DIR}/asset/form-prefix-file.json"
-  assert_gt_zero_points
+  assert_zero_points
   assert_success
 }
 
diff --git a/test/asset/score-0-dep-apparmor-empty-securitycontext.yml b/test/asset/score-0-dep-apparmor-empty-securitycontext.yml
new file mode 100644
index 0000000..5d57384
--- /dev/null
+++ b/test/asset/score-0-dep-apparmor-empty-securitycontext.yml
@@ -0,0 +1,18 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
diff --git a/test/asset/score-0-dep-apparmor-unconfined-container.yml b/test/asset/score-0-dep-apparmor-unconfined-container.yml
new file mode 100644
index 0000000..7d2cddb
--- /dev/null
+++ b/test/asset/score-0-dep-apparmor-unconfined-container.yml
@@ -0,0 +1,21 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+        securityContext:
+          appArmorProfile:
+            type: Unconfined
diff --git a/test/asset/score-0-dep-apparmor-unconfined-ephemeralcontainer.yml b/test/asset/score-0-dep-apparmor-unconfined-ephemeralcontainer.yml
new file mode 100644
index 0000000..8547979
--- /dev/null
+++ b/test/asset/score-0-dep-apparmor-unconfined-ephemeralcontainer.yml
@@ -0,0 +1,24 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+      ephemeralContainers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-ephemeralcontainer
+        securityContext:
+          appArmorProfile:
+            type: Unconfined
diff --git a/test/asset/score-0-dep-apparmor-unconfined-initcontainer.yml b/test/asset/score-0-dep-apparmor-unconfined-initcontainer.yml
new file mode 100644
index 0000000..219dd90
--- /dev/null
+++ b/test/asset/score-0-dep-apparmor-unconfined-initcontainer.yml
@@ -0,0 +1,24 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+      initContainers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-ephemeralcontainer
+        securityContext:
+          appArmorProfile:
+            type: Unconfined
diff --git a/test/asset/score-0-dep-apparmor-unconfined-spec-securitycontext.yml b/test/asset/score-0-dep-apparmor-unconfined-spec-securitycontext.yml
new file mode 100644
index 0000000..97aa4bf
--- /dev/null
+++ b/test/asset/score-0-dep-apparmor-unconfined-spec-securitycontext.yml
@@ -0,0 +1,21 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      securityContext:
+        appArmorProfile:
+          type: Unconfined
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
diff --git a/test/asset/score-1-dep-apparmor-nonunconfined-container.yml b/test/asset/score-1-dep-apparmor-nonunconfined-container.yml
new file mode 100644
index 0000000..8ad2781
--- /dev/null
+++ b/test/asset/score-1-dep-apparmor-nonunconfined-container.yml
@@ -0,0 +1,21 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+        securityContext:
+          appArmorProfile:
+            type: RuntimeDefault
diff --git a/test/asset/score-1-dep-apparmor-nonunconfined-ephemeralcontainer.yml b/test/asset/score-1-dep-apparmor-nonunconfined-ephemeralcontainer.yml
new file mode 100644
index 0000000..fbb2b55
--- /dev/null
+++ b/test/asset/score-1-dep-apparmor-nonunconfined-ephemeralcontainer.yml
@@ -0,0 +1,24 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+      ephemeralContainers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-initcontainer
+        securityContext:
+          appArmorProfile:
+            type: RuntimeDefault
diff --git a/test/asset/score-1-dep-apparmor-nonunconfined-initcontainer.yml b/test/asset/score-1-dep-apparmor-nonunconfined-initcontainer.yml
new file mode 100644
index 0000000..385f67c
--- /dev/null
+++ b/test/asset/score-1-dep-apparmor-nonunconfined-initcontainer.yml
@@ -0,0 +1,24 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
+      initContainers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-initcontainer
+        securityContext:
+          appArmorProfile:
+            type: RuntimeDefault
diff --git a/test/asset/score-1-dep-apparmor-nonunconfined-spec-securitycontext.yml b/test/asset/score-1-dep-apparmor-nonunconfined-spec-securitycontext.yml
new file mode 100644
index 0000000..b1ec479
--- /dev/null
+++ b/test/asset/score-1-dep-apparmor-nonunconfined-spec-securitycontext.yml
@@ -0,0 +1,21 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: trustworthy-deployment
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: trustworthy-deployment
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: trustworthy-deployment
+    spec:
+      securityContext:
+        appArmorProfile:
+          type: RuntimeDefault
+      containers:
+      - image: sotrustworthy:latest
+        name: sotrustworthy-container
diff --git a/test/asset/score-3-pod-apparmor.yaml b/test/asset/score-3-pod-apparmor.yaml
deleted file mode 100644
index 253dd2f..0000000
--- a/test/asset/score-3-pod-apparmor.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# The example Pod utilizing the profile loaded by the sample daemon.
-
-apiVersion: v1
-kind: Pod
-metadata:
-  name: nginx-apparmor
-  # Note that the Pod does not need to be in the same namespace as the loader.
-  labels:
-    app: nginx
-  annotations:
-    # Tell Kubernetes to apply the AppArmor profile "k8s-nginx".
-    # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater.
-    container.apparmor.security.beta.kubernetes.io/nginx: localhost/k8s-nginx
-spec:
-  containers:
-  - name: nginx
-    image: nginx
-    ports:
-    - containerPort: 80