Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 118 additions & 16 deletions assets/queries/k8s/missing_app_armor_config/query.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
not isValidAppArmorProfile(annotations[expectedKey])
ek := annotations[expectedKey])
not isValidAppArmorProfile(ek)

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),
}
}

Expand All @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use get_nested_values_info instead

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, "."), [])
}
Comment on lines +182 to 187
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} 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, "."), [])
}
} else = searchLine {
searchLine := common_lib.build_search_line(split(path, "."), [])
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ spec:
containers:
- name: hello
image: busybox
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
26 changes: 26 additions & 0 deletions assets/queries/k8s/missing_app_armor_config/test/negative2.yaml
Original file line number Diff line number Diff line change
@@ -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" ]
57 changes: 57 additions & 0 deletions assets/queries/k8s/missing_app_armor_config/test/positive2.yaml
Original file line number Diff line number Diff line change
@@ -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" ]
24 changes: 24 additions & 0 deletions assets/queries/k8s/missing_app_armor_config/test/positive3.yaml
Original file line number Diff line number Diff line change
@@ -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" ]
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
Loading