mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Borrowed from rego-cpp Significantly (> 10 times) faster execution. $ cargo test -r --test aci aci/mount_device passed 9.597958ms aci/mount_overlay passed 10.159208ms aci/scratch_mount passed 8.598875ms aci/create_container passed 10.237292ms aci/shutdown_container passed 6.904084ms aci/scratch_unmount passed 6.530875ms aci/unmount_overlay passed 5.958875ms aci/unmount_device passed 5.657834ms aci/load_fragment passed 6.049917ms Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1832 lines
53 KiB
Rego
1832 lines
53 KiB
Rego
# Copyright (c) Microsoft Corporation.
|
|
# Licensed under the MIT License.
|
|
|
|
package framework
|
|
|
|
import future.keywords.every
|
|
import future.keywords.in
|
|
|
|
version := "0.3.0"
|
|
|
|
device_mounted(target) {
|
|
data.metadata.devices[target]
|
|
}
|
|
|
|
default deviceHash_ok := false
|
|
|
|
# test if a device hash exists as a layer in a policy container
|
|
deviceHash_ok {
|
|
layer := data.policy.containers[_].layers[_]
|
|
input.deviceHash == layer
|
|
}
|
|
|
|
# test if a device hash exists as a layer in a fragment container
|
|
deviceHash_ok {
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
some fragment in feed
|
|
layer := fragment.containers[_].layers[_]
|
|
input.deviceHash == layer
|
|
}
|
|
|
|
default mount_device := {"allowed": false}
|
|
|
|
mount_device := {"metadata": [addDevice], "allowed": true} {
|
|
not device_mounted(input.target)
|
|
deviceHash_ok
|
|
addDevice := {
|
|
"name": "devices",
|
|
"action": "add",
|
|
"key": input.target,
|
|
"value": input.deviceHash,
|
|
}
|
|
}
|
|
|
|
default unmount_device := {"allowed": false}
|
|
|
|
unmount_device := {"metadata": [removeDevice], "allowed": true} {
|
|
device_mounted(input.unmountTarget)
|
|
removeDevice := {
|
|
"name": "devices",
|
|
"action": "remove",
|
|
"key": input.unmountTarget,
|
|
}
|
|
}
|
|
|
|
layerPaths_ok(layers) {
|
|
length := count(layers)
|
|
count(input.layerPaths) == length
|
|
every i, path in input.layerPaths {
|
|
layers[(length - i) - 1] == data.metadata.devices[path]
|
|
}
|
|
}
|
|
|
|
default overlay_exists := false
|
|
|
|
overlay_exists {
|
|
data.metadata.matches[input.containerID]
|
|
}
|
|
|
|
overlay_mounted(target) {
|
|
data.metadata.overlayTargets[target]
|
|
}
|
|
|
|
default candidate_containers := []
|
|
|
|
candidate_containers := containers {
|
|
semver.compare(policy_framework_version, version) == 0
|
|
|
|
policy_containers := [c | c := data.policy.containers[_]]
|
|
fragment_containers := [c |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
c := fragment.containers[_]
|
|
]
|
|
|
|
containers := array.concat(policy_containers, fragment_containers)
|
|
}
|
|
|
|
candidate_containers := containers {
|
|
semver.compare(policy_framework_version, version) < 0
|
|
|
|
policy_containers := apply_defaults("container", data.policy.containers, policy_framework_version)
|
|
fragment_containers := [c |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
c := fragment.containers[_]
|
|
]
|
|
|
|
containers := array.concat(policy_containers, fragment_containers)
|
|
}
|
|
|
|
default mount_overlay := {"allowed": false}
|
|
|
|
mount_overlay := {"metadata": [addMatches, addOverlayTarget], "allowed": true} {
|
|
not overlay_exists
|
|
|
|
containers := [container |
|
|
container := candidate_containers[_]
|
|
layerPaths_ok(container.layers)
|
|
]
|
|
|
|
count(containers) > 0
|
|
addMatches := {
|
|
"name": "matches",
|
|
"action": "add",
|
|
"key": input.containerID,
|
|
"value": containers,
|
|
}
|
|
|
|
addOverlayTarget := {
|
|
"name": "overlayTargets",
|
|
"action": "add",
|
|
"key": input.target,
|
|
"value": true,
|
|
}
|
|
}
|
|
|
|
default unmount_overlay := {"allowed": false}
|
|
|
|
unmount_overlay := {"metadata": [removeOverlayTarget], "allowed": true} {
|
|
overlay_mounted(input.unmountTarget)
|
|
removeOverlayTarget := {
|
|
"name": "overlayTargets",
|
|
"action": "remove",
|
|
"key": input.unmountTarget,
|
|
}
|
|
}
|
|
|
|
command_ok(command) {
|
|
count(input.argList) == count(command)
|
|
every i, arg in input.argList {
|
|
command[i] == arg
|
|
}
|
|
}
|
|
|
|
env_ok(pattern, "string", value) {
|
|
pattern == value
|
|
}
|
|
|
|
env_ok(pattern, "re2", value) {
|
|
regex.match(pattern, value)
|
|
}
|
|
|
|
rule_ok(rule, env) {
|
|
not rule.required
|
|
}
|
|
|
|
rule_ok(rule, env) {
|
|
rule.required
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
|
|
envList_ok(env_rules, envList) {
|
|
every rule in env_rules {
|
|
some env in envList
|
|
rule_ok(rule, env)
|
|
}
|
|
|
|
every env in envList {
|
|
some rule in env_rules
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
}
|
|
|
|
valid_envs_subset(env_rules) := envs {
|
|
envs := {env |
|
|
some env in input.envList
|
|
some rule in env_rules
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
}
|
|
|
|
valid_envs_for_all(items) := envs {
|
|
allow_environment_variable_dropping
|
|
|
|
# for each item, find a subset of the environment rules
|
|
# that are valid
|
|
valid := [envs |
|
|
some item in items
|
|
envs := valid_envs_subset(item.env_rules)
|
|
]
|
|
|
|
# we want to select the most specific matches, which in this
|
|
# case consists of those matches which require dropping the
|
|
# fewest environment variables (i.e. the longest lists)
|
|
counts := [num_envs |
|
|
envs := valid[_]
|
|
num_envs := count(envs)
|
|
]
|
|
max_count := max(counts)
|
|
|
|
largest_env_sets := {envs |
|
|
some i
|
|
counts[i] == max_count
|
|
envs := valid[i]
|
|
}
|
|
|
|
# if there is more than one set with the same size, we
|
|
# can only proceed if they are all the same, so we verify
|
|
# that the intersection is equal to the union. For a single
|
|
# set this is trivially true.
|
|
envs_i := intersection(largest_env_sets)
|
|
envs_u := union(largest_env_sets)
|
|
envs_i == envs_u
|
|
envs := envs_i
|
|
}
|
|
|
|
valid_envs_for_all(items) := envs {
|
|
not allow_environment_variable_dropping
|
|
|
|
# no dropping allowed, so we just return the input
|
|
envs := input.envList
|
|
}
|
|
|
|
workingDirectory_ok(working_dir) {
|
|
input.workingDir == working_dir
|
|
}
|
|
|
|
privileged_ok(elevation_allowed) {
|
|
not input.privileged
|
|
}
|
|
|
|
privileged_ok(elevation_allowed) {
|
|
input.privileged
|
|
input.privileged == elevation_allowed
|
|
}
|
|
|
|
noNewPrivileges_ok(no_new_privileges) {
|
|
no_new_privileges
|
|
input.noNewPrivileges
|
|
}
|
|
|
|
noNewPrivileges_ok(no_new_privileges) {
|
|
no_new_privileges == false
|
|
}
|
|
|
|
idName_ok(pattern, "any", value) {
|
|
true
|
|
}
|
|
|
|
idName_ok(pattern, "id", value) {
|
|
pattern == value.id
|
|
}
|
|
|
|
idName_ok(pattern, "name", value) {
|
|
pattern == value.name
|
|
}
|
|
|
|
idName_ok(pattern, "re2", value) {
|
|
regex.match(pattern, value.name)
|
|
}
|
|
|
|
user_ok(user) {
|
|
user.umask == input.umask
|
|
idName_ok(user.user_idname.pattern, user.user_idname.strategy, input.user)
|
|
every group in input.groups {
|
|
some group_idname in user.group_idnames
|
|
idName_ok(group_idname.pattern, group_idname.strategy, group)
|
|
}
|
|
}
|
|
|
|
seccomp_ok(seccomp_profile_sha256) {
|
|
input.seccompProfileSHA256 == seccomp_profile_sha256
|
|
}
|
|
|
|
default container_started := false
|
|
|
|
container_started {
|
|
data.metadata.started[input.containerID]
|
|
}
|
|
|
|
default container_privileged := false
|
|
|
|
container_privileged {
|
|
data.metadata.started[input.containerID].privileged
|
|
}
|
|
|
|
capsList_ok(allowed_caps_list, requested_caps_list) {
|
|
count(allowed_caps_list) == count(requested_caps_list)
|
|
|
|
every cap in requested_caps_list {
|
|
some allowed in allowed_caps_list
|
|
cap == allowed
|
|
}
|
|
|
|
every allowed in allowed_caps_list {
|
|
some cap in requested_caps_list
|
|
allowed == cap
|
|
}
|
|
}
|
|
|
|
filter_capsList_by_allowed(allowed_caps_list, requested_caps_list) := caps {
|
|
# find a subset of the capabilities that are valid
|
|
caps := {cap |
|
|
some cap in requested_caps_list
|
|
some allowed in allowed_caps_list
|
|
cap == allowed
|
|
}
|
|
}
|
|
|
|
filter_capsList_for_single_container(allowed_caps) := caps {
|
|
bounding := filter_capsList_by_allowed(allowed_caps.bounding, input.capabilities.bounding)
|
|
effective := filter_capsList_by_allowed(allowed_caps.effective, input.capabilities.effective)
|
|
inheritable := filter_capsList_by_allowed(allowed_caps.inheritable, input.capabilities.inheritable)
|
|
permitted := filter_capsList_by_allowed(allowed_caps.permitted, input.capabilities.permitted)
|
|
ambient := filter_capsList_by_allowed(allowed_caps.ambient, input.capabilities.ambient)
|
|
|
|
caps := {
|
|
"bounding": bounding,
|
|
"effective": effective,
|
|
"inheritable": inheritable,
|
|
"permitted": permitted,
|
|
"ambient": ambient
|
|
}
|
|
}
|
|
|
|
largest_caps_sets_for_all(containers, privileged) := largest_caps_sets {
|
|
filtered := [caps |
|
|
container := containers[_]
|
|
capabilities := get_capabilities(container, privileged)
|
|
caps := filter_capsList_for_single_container(capabilities)
|
|
]
|
|
|
|
# we want to select the most specific matches, which in this
|
|
# case consists of those matches which require dropping the
|
|
# fewest capabilities (i.e. the longest lists)
|
|
counts := [num_caps |
|
|
caps := filtered[_]
|
|
num_caps := count(caps.bounding) + count(caps.effective) +
|
|
count(caps.inheritable) + count(caps.permitted) +
|
|
count(caps.ambient)
|
|
]
|
|
max_count := max(counts)
|
|
|
|
largest_caps_sets := [caps |
|
|
some i
|
|
counts[i] == max_count
|
|
caps := filtered[i]
|
|
]
|
|
}
|
|
|
|
all_caps_sets_are_equal(sets) := caps {
|
|
# if there is more than one set with the same size, we
|
|
# can only proceed if they are all the same, so we verify
|
|
# that the intersection is equal to the union. For a single
|
|
# set this is trivially true.
|
|
bounding_i := intersection({caps.bounding | caps := sets[_]})
|
|
effective_i := intersection({caps.effective | caps := sets[_]})
|
|
inheritable_i := intersection({caps.inheritable | caps := sets[_]})
|
|
permitted_i := intersection({caps.permitted | caps := sets[_]})
|
|
ambient_i := intersection({caps.ambient | caps := sets[_]})
|
|
|
|
bounding_u := union({caps.bounding | caps := sets[_]})
|
|
effective_u := union({caps.effective | caps := sets[_]})
|
|
inheritable_u := union({caps.inheritable | caps := sets[_]})
|
|
permitted_u := union({caps.permitted | caps := sets[_]})
|
|
ambient_u := union({caps.ambient | caps := sets[_]})
|
|
|
|
bounding_i == bounding_u
|
|
effective_i == effective_u
|
|
inheritable_i == inheritable_u
|
|
permitted_i == permitted_u
|
|
ambient_i == ambient_u
|
|
|
|
caps := {
|
|
"bounding": bounding_i,
|
|
"effective": effective_i,
|
|
"inheritable": inheritable_i,
|
|
"permitted": permitted_i,
|
|
"ambient": ambient_i,
|
|
}
|
|
}
|
|
|
|
valid_caps_for_all(containers, privileged) := caps {
|
|
allow_capability_dropping
|
|
|
|
# find largest matching capabilities sets aka "the most specific"
|
|
largest_caps_sets := largest_caps_sets_for_all(containers, privileged)
|
|
|
|
# if there is more than one set with the same size, we
|
|
# can only proceed if they are all the same
|
|
caps := all_caps_sets_are_equal(largest_caps_sets)
|
|
}
|
|
|
|
valid_caps_for_all(containers, privileged) := caps {
|
|
not allow_capability_dropping
|
|
|
|
# no dropping allowed, so we just return the input
|
|
caps := input.capabilities
|
|
}
|
|
|
|
caps_ok(allowed_caps, requested_caps) {
|
|
capsList_ok(allowed_caps.bounding, requested_caps.bounding)
|
|
capsList_ok(allowed_caps.effective, requested_caps.effective)
|
|
capsList_ok(allowed_caps.inheritable, requested_caps.inheritable)
|
|
capsList_ok(allowed_caps.permitted, requested_caps.permitted)
|
|
capsList_ok(allowed_caps.ambient, requested_caps.ambient)
|
|
}
|
|
|
|
get_capabilities(container, privileged) := capabilities {
|
|
container.capabilities != null
|
|
capabilities := container.capabilities
|
|
}
|
|
|
|
default_privileged_capabilities := capabilities {
|
|
caps := {cap | cap := data.defaultPrivilegedCapabilities[_]}
|
|
capabilities := {
|
|
"bounding": caps,
|
|
"effective": caps,
|
|
"inheritable": caps,
|
|
"permitted": caps,
|
|
"ambient": set(),
|
|
}
|
|
}
|
|
|
|
get_capabilities(container, true) := capabilities {
|
|
container.capabilities == null
|
|
container.allow_elevated
|
|
capabilities := default_privileged_capabilities
|
|
}
|
|
|
|
default_unprivileged_capabilities := capabilities {
|
|
caps := {cap | cap := data.defaultUnprivilegedCapabilities[_]}
|
|
capabilities := {
|
|
"bounding": caps,
|
|
"effective": caps,
|
|
"inheritable": set(),
|
|
"permitted": caps,
|
|
"ambient": set(),
|
|
}
|
|
}
|
|
|
|
get_capabilities(container, false) := capabilities {
|
|
container.capabilities == null
|
|
container.allow_elevated
|
|
capabilities := default_unprivileged_capabilities
|
|
}
|
|
|
|
get_capabilities(container, privileged) := capabilities {
|
|
container.capabilities == null
|
|
not container.allow_elevated
|
|
capabilities := default_unprivileged_capabilities
|
|
}
|
|
|
|
default create_container := {"allowed": false}
|
|
|
|
create_container := {"metadata": [updateMatches, addStarted],
|
|
"env_list": env_list,
|
|
"caps_list": caps_list,
|
|
"allow_stdio_access": allow_stdio_access,
|
|
"allowed": true} {
|
|
not container_started
|
|
|
|
# narrow the matches based upon command, working directory, and
|
|
# mount list
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
# NB any change to these narrowing conditions should be reflected in
|
|
# the error handling, such that error messaging correctly reflects
|
|
# the narrowing process.
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
privileged_ok(container.allow_elevated)
|
|
workingDirectory_ok(container.working_dir)
|
|
command_ok(container.command)
|
|
mountList_ok(container.mounts, container.allow_elevated)
|
|
seccomp_ok(container.seccomp_profile_sha256)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
# check to see if the capabilities variables match, dropping
|
|
# them if allowed (and necessary)
|
|
caps_list := valid_caps_for_all(possible_after_env_containers, input.privileged)
|
|
possible_after_caps_containers := [container |
|
|
container := possible_after_env_containers[_]
|
|
caps_ok(get_capabilities(container, input.privileged), caps_list)
|
|
]
|
|
|
|
count(possible_after_caps_containers) > 0
|
|
|
|
# set final container list
|
|
containers := possible_after_caps_containers
|
|
|
|
# we can't do narrowing based on allowing stdio access so at this point
|
|
# every container from the policy that might match this create request
|
|
# must have the same allow stdio value otherwise, we are in an undecidable
|
|
# state
|
|
allow_stdio_access := containers[0].allow_stdio_access
|
|
every c in containers {
|
|
c.allow_stdio_access == allow_stdio_access
|
|
}
|
|
|
|
updateMatches := {
|
|
"name": "matches",
|
|
"action": "update",
|
|
"key": input.containerID,
|
|
"value": containers,
|
|
}
|
|
|
|
addStarted := {
|
|
"name": "started",
|
|
"action": "add",
|
|
"key": input.containerID,
|
|
"value": {
|
|
"privileged": input.privileged,
|
|
},
|
|
}
|
|
}
|
|
|
|
mountSource_ok(constraint, source) {
|
|
startswith(constraint, data.sandboxPrefix)
|
|
newConstraint := replace(constraint, data.sandboxPrefix, input.sandboxDir)
|
|
regex.match(newConstraint, source)
|
|
}
|
|
|
|
mountSource_ok(constraint, source) {
|
|
startswith(constraint, data.hugePagesPrefix)
|
|
newConstraint := replace(constraint, data.hugePagesPrefix, input.hugePagesDir)
|
|
regex.match(newConstraint, source)
|
|
}
|
|
|
|
mountSource_ok(constraint, source) {
|
|
startswith(constraint, data.plan9Prefix)
|
|
some target, containerID in data.metadata.p9mounts
|
|
source == target
|
|
input.containerID == containerID
|
|
}
|
|
|
|
mountSource_ok(constraint, source) {
|
|
constraint == source
|
|
}
|
|
|
|
mountConstraint_ok(constraint, mount) {
|
|
mount.type == constraint.type
|
|
mountSource_ok(constraint.source, mount.source)
|
|
mount.destination != ""
|
|
mount.destination == constraint.destination
|
|
|
|
# the following check is not required (as the following tests will prove this
|
|
# condition as well), however it will check whether those more expensive
|
|
# tests need to be performed.
|
|
count(mount.options) == count(constraint.options)
|
|
every option in mount.options {
|
|
some constraintOption in constraint.options
|
|
option == constraintOption
|
|
}
|
|
|
|
every option in constraint.options {
|
|
some mountOption in mount.options
|
|
option == mountOption
|
|
}
|
|
}
|
|
|
|
mount_ok(mounts, allow_elevated, mount) {
|
|
some constraint in mounts
|
|
mountConstraint_ok(constraint, mount)
|
|
}
|
|
|
|
mount_ok(mounts, allow_elevated, mount) {
|
|
some constraint in data.defaultMounts
|
|
mountConstraint_ok(constraint, mount)
|
|
}
|
|
|
|
mount_ok(mounts, allow_elevated, mount) {
|
|
allow_elevated
|
|
some constraint in data.privilegedMounts
|
|
mountConstraint_ok(constraint, mount)
|
|
}
|
|
|
|
mountList_ok(mounts, allow_elevated) {
|
|
every mount in input.mounts {
|
|
mount_ok(mounts, allow_elevated, mount)
|
|
}
|
|
}
|
|
|
|
default exec_in_container := {"allowed": false}
|
|
|
|
exec_in_container := {"metadata": [updateMatches],
|
|
"env_list": env_list,
|
|
"caps_list": caps_list,
|
|
"allowed": true} {
|
|
container_started
|
|
|
|
# narrow our matches based upon the process requested
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
# NB any change to these narrowing conditions should be reflected in
|
|
# the error handling, such that error messaging correctly reflects
|
|
# the narrowing process.
|
|
workingDirectory_ok(container.working_dir)
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
# check to see if the capabilities variables match, dropping
|
|
# them if allowed (and necessary)
|
|
caps_list := valid_caps_for_all(possible_after_env_containers, container_privileged)
|
|
possible_after_caps_containers := [container |
|
|
container := possible_after_env_containers[_]
|
|
caps_ok(get_capabilities(container, container_privileged), caps_list)
|
|
]
|
|
|
|
count(possible_after_caps_containers) > 0
|
|
|
|
# set final container list
|
|
containers := possible_after_caps_containers
|
|
|
|
updateMatches := {
|
|
"name": "matches",
|
|
"action": "update",
|
|
"key": input.containerID,
|
|
"value": containers,
|
|
}
|
|
}
|
|
|
|
default shutdown_container := {"allowed": false}
|
|
|
|
shutdown_container := {"metadata": [remove], "allowed": true} {
|
|
container_started
|
|
remove := {
|
|
"name": "matches",
|
|
"action": "remove",
|
|
"key": input.containerID,
|
|
}
|
|
}
|
|
|
|
default signal_container_process := {"allowed": false}
|
|
|
|
signal_container_process := {"metadata": [updateMatches], "allowed": true} {
|
|
container_started
|
|
input.isInitProcess
|
|
containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
signal_ok(container.signals)
|
|
]
|
|
|
|
count(containers) > 0
|
|
updateMatches := {
|
|
"name": "matches",
|
|
"action": "update",
|
|
"key": input.containerID,
|
|
"value": containers,
|
|
}
|
|
}
|
|
|
|
signal_container_process := {"metadata": [updateMatches], "allowed": true} {
|
|
container_started
|
|
not input.isInitProcess
|
|
containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
signal_ok(process.signals)
|
|
]
|
|
|
|
count(containers) > 0
|
|
updateMatches := {
|
|
"name": "matches",
|
|
"action": "update",
|
|
"key": input.containerID,
|
|
"value": containers,
|
|
}
|
|
}
|
|
|
|
signal_ok(signals) {
|
|
some signal in signals
|
|
input.signal == signal
|
|
}
|
|
|
|
plan9_mounted(target) {
|
|
data.metadata.p9mounts[target]
|
|
}
|
|
|
|
default plan9_mount := {"allowed": false}
|
|
|
|
plan9_mount := {"metadata": [addPlan9Target], "allowed": true} {
|
|
not plan9_mounted(input.target)
|
|
some containerID, _ in data.metadata.matches
|
|
pattern := concat("", [input.rootPrefix, "/", containerID, input.mountPathPrefix])
|
|
regex.match(pattern, input.target)
|
|
addPlan9Target := {
|
|
"name": "p9mounts",
|
|
"action": "add",
|
|
"key": input.target,
|
|
"value": containerID,
|
|
}
|
|
}
|
|
|
|
default plan9_unmount := {"allowed": false}
|
|
|
|
plan9_unmount := {"metadata": [removePlan9Target], "allowed": true} {
|
|
plan9_mounted(input.unmountTarget)
|
|
removePlan9Target := {
|
|
"name": "p9mounts",
|
|
"action": "remove",
|
|
"key": input.unmountTarget,
|
|
}
|
|
}
|
|
|
|
|
|
default enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": true, "invalid": false, "version_missing": false}
|
|
|
|
enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": false, "version_missing": true} {
|
|
policy_api_version == null
|
|
}
|
|
|
|
enforcement_point_info := {"available": available, "default_results": default_results, "unknown": false, "invalid": false, "version_missing": false} {
|
|
enforcement_point := data.api.enforcement_points[input.name]
|
|
semver.compare(data.api.version, enforcement_point.introducedVersion) >= 0
|
|
available := semver.compare(policy_api_version, enforcement_point.introducedVersion) >= 0
|
|
default_results := enforcement_point.default_results
|
|
}
|
|
|
|
enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": true, "version_missing": false} {
|
|
enforcement_point := data.api.enforcement_points[input.name]
|
|
semver.compare(data.api.version, enforcement_point.introducedVersion) < 0
|
|
}
|
|
|
|
default candidate_external_processes := []
|
|
|
|
candidate_external_processes := external_processes {
|
|
semver.compare(policy_framework_version, version) == 0
|
|
|
|
policy_external_processes := [e | e := data.policy.external_processes[_]]
|
|
fragment_external_processes := [e |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
e := fragment.external_processes[_]
|
|
]
|
|
|
|
external_processes := array.concat(policy_external_processes, fragment_external_processes)
|
|
}
|
|
|
|
candidate_external_processes := external_processes {
|
|
semver.compare(policy_framework_version, version) < 0
|
|
|
|
policy_external_processes := apply_defaults("external_process", data.policy.external_processes, policy_framework_version)
|
|
fragment_external_processes := [e |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
e := fragment.external_processes[_]
|
|
]
|
|
|
|
external_processes := array.concat(policy_external_processes, fragment_external_processes)
|
|
}
|
|
|
|
external_process_ok(process) {
|
|
command_ok(process.command)
|
|
envList_ok(process.env_rules, input.envList)
|
|
workingDirectory_ok(process.working_dir)
|
|
}
|
|
|
|
default exec_external := {"allowed": false}
|
|
|
|
exec_external := {"allowed": true,
|
|
"allow_stdio_access": allow_stdio_access,
|
|
"env_list": env_list} {
|
|
possible_processes := [process |
|
|
process := candidate_external_processes[_]
|
|
# NB any change to these narrowing conditions should be reflected in
|
|
# the error handling, such that error messaging correctly reflects
|
|
# the narrowing process.
|
|
workingDirectory_ok(process.working_dir)
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_processes) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_processes)
|
|
processes := [process |
|
|
process := possible_processes[_]
|
|
envList_ok(process.env_rules, env_list)
|
|
]
|
|
|
|
count(processes) > 0
|
|
|
|
allow_stdio_access := processes[0].allow_stdio_access
|
|
every p in processes {
|
|
p.allow_stdio_access == allow_stdio_access
|
|
}
|
|
}
|
|
|
|
default get_properties := {"allowed": false}
|
|
|
|
get_properties := {"allowed": true} {
|
|
allow_properties_access
|
|
}
|
|
|
|
default dump_stacks := {"allowed": false}
|
|
|
|
dump_stacks := {"allowed": true} {
|
|
allow_dump_stacks
|
|
}
|
|
|
|
default runtime_logging := {"allowed": false}
|
|
|
|
runtime_logging := {"allowed": true} {
|
|
allow_runtime_logging
|
|
}
|
|
|
|
default fragment_containers := []
|
|
|
|
fragment_containers := data[input.namespace].containers
|
|
|
|
default fragment_fragments := []
|
|
|
|
fragment_fragments := data[input.namespace].fragments
|
|
|
|
default fragment_external_processes := []
|
|
|
|
fragment_external_processes := data[input.namespace].external_processes
|
|
|
|
apply_defaults(name, raw_values, framework_version) := values {
|
|
semver.compare(framework_version, version) == 0
|
|
values := raw_values
|
|
}
|
|
|
|
apply_defaults("container", raw_values, framework_version) := values {
|
|
semver.compare(framework_version, version) < 0
|
|
values := [checked |
|
|
raw := raw_values[_]
|
|
checked := check_container(raw, framework_version)
|
|
]
|
|
}
|
|
|
|
apply_defaults("external_process", raw_values, framework_version) := values {
|
|
semver.compare(framework_version, version) < 0
|
|
values := [checked |
|
|
raw := raw_values[_]
|
|
checked := check_external_process(raw, framework_version)
|
|
]
|
|
}
|
|
|
|
apply_defaults("fragment", raw_values, framework_version) := values {
|
|
semver.compare(framework_version, version) < 0
|
|
values := [checked |
|
|
raw := raw_values[_]
|
|
checked := check_fragment(raw, framework_version)
|
|
]
|
|
}
|
|
|
|
default fragment_framework_version := null
|
|
fragment_framework_version := data[input.namespace].framework_version
|
|
|
|
extract_fragment_includes(includes) := fragment {
|
|
framework_version := fragment_framework_version
|
|
objects := {
|
|
"containers": apply_defaults("container", fragment_containers, framework_version),
|
|
"fragments": apply_defaults("fragment", fragment_fragments, framework_version),
|
|
"external_processes": apply_defaults("external_process", fragment_external_processes, framework_version)
|
|
}
|
|
|
|
fragment := {
|
|
include: objects[include] | include := includes[_]
|
|
}
|
|
}
|
|
|
|
issuer_exists(iss) {
|
|
data.metadata.issuers[iss]
|
|
}
|
|
|
|
feed_exists(iss, feed) {
|
|
data.metadata.issuers[iss].feeds[feed]
|
|
}
|
|
|
|
update_issuer(includes) := issuer {
|
|
feed_exists(input.issuer, input.feed)
|
|
old_issuer := data.metadata.issuers[input.issuer]
|
|
old_fragments := old_issuer.feeds[input.feed]
|
|
new_issuer := {"feeds": {input.feed: array.concat([extract_fragment_includes(includes)], old_fragments)}}
|
|
|
|
issuer := object.union(old_issuer, new_issuer)
|
|
}
|
|
|
|
update_issuer(includes) := issuer {
|
|
not feed_exists(input.issuer, input.feed)
|
|
old_issuer := data.metadata.issuers[input.issuer]
|
|
new_issuer := {"feeds": {input.feed: [extract_fragment_includes(includes)]}}
|
|
|
|
issuer := object.union(old_issuer, new_issuer)
|
|
}
|
|
|
|
update_issuer(includes) := issuer {
|
|
not issuer_exists(input.issuer)
|
|
issuer := {"feeds": {input.feed: [extract_fragment_includes(includes)]}}
|
|
}
|
|
|
|
default candidate_fragments := []
|
|
|
|
candidate_fragments := fragments {
|
|
semver.compare(policy_framework_version, version) == 0
|
|
|
|
policy_fragments := [f | f := data.policy.fragments[_]]
|
|
fragment_fragments := [f |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
f := fragment.fragments[_]
|
|
]
|
|
|
|
fragments := array.concat(policy_fragments, fragment_fragments)
|
|
}
|
|
|
|
candidate_fragments := fragments {
|
|
semver.compare(policy_framework_version, version) < 0
|
|
|
|
policy_fragments := apply_defaults("fragment", data.policy.fragments, policy_framework_version)
|
|
fragment_fragments := [f |
|
|
feed := data.metadata.issuers[_].feeds[_]
|
|
fragment := feed[_]
|
|
f := fragment.fragments[_]
|
|
]
|
|
|
|
fragments := array.concat(policy_fragments, fragment_fragments)
|
|
}
|
|
|
|
default load_fragment := {"allowed": false}
|
|
|
|
svn_ok(svn, minimum_svn) {
|
|
# deprecated
|
|
semver.is_valid(svn)
|
|
semver.is_valid(minimum_svn)
|
|
semver.compare(svn, minimum_svn) >= 0
|
|
}
|
|
|
|
svn_ok(svn, minimum_svn) {
|
|
to_number(svn) >= to_number(minimum_svn)
|
|
}
|
|
|
|
fragment_ok(fragment) {
|
|
input.issuer == fragment.issuer
|
|
input.feed == fragment.feed
|
|
svn_ok(data[input.namespace].svn, fragment.minimum_svn)
|
|
}
|
|
|
|
load_fragment := {"metadata": [updateIssuer], "add_module": add_module, "allowed": true} {
|
|
some fragment in candidate_fragments
|
|
fragment_ok(fragment)
|
|
|
|
issuer := update_issuer(fragment.includes)
|
|
updateIssuer := {
|
|
"name": "issuers",
|
|
"action": "update",
|
|
"key": input.issuer,
|
|
"value": issuer,
|
|
}
|
|
|
|
add_module := "namespace" in fragment.includes
|
|
}
|
|
|
|
default scratch_mount := {"allowed": false}
|
|
|
|
scratch_mounted(target) {
|
|
data.metadata.scratch_mounts[target]
|
|
}
|
|
|
|
scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} {
|
|
not scratch_mounted(input.target)
|
|
allow_unencrypted_scratch
|
|
add_scratch_mount := {
|
|
"name": "scratch_mounts",
|
|
"action": "add",
|
|
"key": input.target,
|
|
"value": {"encrypted": input.encrypted},
|
|
}
|
|
}
|
|
|
|
scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} {
|
|
not scratch_mounted(input.target)
|
|
not allow_unencrypted_scratch
|
|
input.encrypted
|
|
add_scratch_mount := {
|
|
"name": "scratch_mounts",
|
|
"action": "add",
|
|
"key": input.target,
|
|
"value": {"encrypted": input.encrypted},
|
|
}
|
|
}
|
|
|
|
default scratch_unmount := {"allowed": false}
|
|
|
|
scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} {
|
|
scratch_mounted(input.unmountTarget)
|
|
remove_scratch_mount := {
|
|
"name": "scratch_mounts",
|
|
"action": "remove",
|
|
"key": input.unmountTarget,
|
|
}
|
|
}
|
|
|
|
reason := {
|
|
"errors": errors,
|
|
"error_objects": error_objects
|
|
}
|
|
|
|
################################################################
|
|
# Error messages
|
|
################################################################
|
|
|
|
errors["deviceHash not found"] {
|
|
input.rule == "mount_device"
|
|
not deviceHash_ok
|
|
}
|
|
|
|
errors["device already mounted at path"] {
|
|
input.rule == "mount_device"
|
|
device_mounted(input.target)
|
|
}
|
|
|
|
errors["no device at path to unmount"] {
|
|
input.rule == "unmount_device"
|
|
not device_mounted(input.unmountTarget)
|
|
}
|
|
|
|
errors["container already started"] {
|
|
input.rule == "create_container"
|
|
container_started
|
|
}
|
|
|
|
errors["container not started"] {
|
|
input.rule in ["exec_in_container", "shutdown_container", "signal_container_process"]
|
|
not container_started
|
|
}
|
|
|
|
errors["overlay has already been mounted"] {
|
|
input.rule == "mount_overlay"
|
|
overlay_exists
|
|
}
|
|
|
|
default overlay_matches := false
|
|
|
|
overlay_matches {
|
|
some container in candidate_containers
|
|
layerPaths_ok(container.layers)
|
|
}
|
|
|
|
errors["no overlay at path to unmount"] {
|
|
input.rule == "unmount_overlay"
|
|
not overlay_mounted(input.unmountTarget)
|
|
}
|
|
|
|
errors["no matching containers for overlay"] {
|
|
input.rule == "mount_overlay"
|
|
not overlay_matches
|
|
}
|
|
|
|
default privileged_matches := false
|
|
|
|
privileged_matches {
|
|
input.rule == "create_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
privileged_ok(container.allow_elevated)
|
|
}
|
|
|
|
errors["privileged escalation not allowed"] {
|
|
input.rule in ["create_container"]
|
|
not privileged_matches
|
|
}
|
|
|
|
default command_matches := false
|
|
|
|
command_matches {
|
|
input.rule == "create_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
command_ok(container.command)
|
|
}
|
|
|
|
command_matches {
|
|
input.rule == "exec_in_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
}
|
|
|
|
command_matches {
|
|
input.rule == "exec_external"
|
|
some process in candidate_external_processes
|
|
command_ok(process.command)
|
|
}
|
|
|
|
errors["invalid command"] {
|
|
input.rule in ["create_container", "exec_in_container", "exec_external"]
|
|
not command_matches
|
|
}
|
|
|
|
env_matches(env) {
|
|
input.rule in ["create_container", "exec_in_container"]
|
|
some container in data.metadata.matches[input.containerID]
|
|
some rule in container.env_rules
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
|
|
env_matches(env) {
|
|
input.rule in ["exec_external"]
|
|
some process in candidate_external_processes
|
|
some rule in process.env_rules
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
|
|
errors[envError] {
|
|
input.rule in ["create_container", "exec_in_container", "exec_external"]
|
|
bad_envs := [invalid |
|
|
env := input.envList[_]
|
|
not env_matches(env)
|
|
parts := split(env, "=")
|
|
invalid = parts[0]
|
|
]
|
|
|
|
count(bad_envs) > 0
|
|
envError := concat(" ", ["invalid env list:", concat(",", bad_envs)])
|
|
}
|
|
|
|
env_rule_matches(rule) {
|
|
some env in input.envList
|
|
env_ok(rule.pattern, rule.strategy, env)
|
|
}
|
|
|
|
errors["missing required environment variable"] {
|
|
input.rule == "create_container"
|
|
|
|
not container_started
|
|
possible_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
privileged_ok(container.allow_elevated)
|
|
workingDirectory_ok(container.working_dir)
|
|
command_ok(container.command)
|
|
mountList_ok(container.mounts, container.allow_elevated)
|
|
]
|
|
|
|
count(possible_containers) > 0
|
|
|
|
containers := [container |
|
|
container := possible_containers[_]
|
|
missing_rules := {invalid |
|
|
invalid := {rule |
|
|
rule := container.env_rules[_]
|
|
rule.required
|
|
not env_rule_matches(rule)
|
|
}
|
|
count(invalid) > 0
|
|
}
|
|
count(missing_rules) > 0
|
|
]
|
|
|
|
count(containers) > 0
|
|
}
|
|
|
|
errors["missing required environment variable"] {
|
|
input.rule == "exec_in_container"
|
|
|
|
container_started
|
|
possible_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
workingDirectory_ok(container.working_dir)
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_containers) > 0
|
|
|
|
containers := [container |
|
|
container := possible_containers[_]
|
|
missing_rules := {invalid |
|
|
invalid := {rule |
|
|
rule := container.env_rules[_]
|
|
rule.required
|
|
not env_rule_matches(rule)
|
|
}
|
|
count(invalid) > 0
|
|
}
|
|
count(missing_rules) > 0
|
|
]
|
|
|
|
count(containers) > 0
|
|
}
|
|
|
|
errors["missing required environment variable"] {
|
|
input.rule == "exec_external"
|
|
|
|
possible_processes := [process |
|
|
process := candidate_external_processes[_]
|
|
workingDirectory_ok(process.working_dir)
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_processes) > 0
|
|
|
|
processes := [process |
|
|
process := possible_processes[_]
|
|
missing_rules := {invalid |
|
|
invalid := {rule |
|
|
rule := process.env_rules[_]
|
|
rule.required
|
|
not env_rule_matches(rule)
|
|
}
|
|
count(invalid) > 0
|
|
}
|
|
count(missing_rules) > 0
|
|
]
|
|
|
|
count(processes) > 0
|
|
}
|
|
|
|
default workingDirectory_matches := false
|
|
|
|
workingDirectory_matches {
|
|
input.rule in ["create_container", "exec_in_container"]
|
|
some container in data.metadata.matches[input.containerID]
|
|
workingDirectory_ok(container.working_dir)
|
|
}
|
|
|
|
workingDirectory_matches {
|
|
input.rule == "exec_external"
|
|
some process in candidate_external_processes
|
|
workingDirectory_ok(process.working_dir)
|
|
}
|
|
|
|
errors["invalid working directory"] {
|
|
input.rule in ["create_container", "exec_in_container", "exec_external"]
|
|
not workingDirectory_matches
|
|
}
|
|
|
|
mount_matches(mount) {
|
|
some container in data.metadata.matches[input.containerID]
|
|
mount_ok(container.mounts, container.allow_elevated, mount)
|
|
}
|
|
|
|
errors[mountError] {
|
|
input.rule == "create_container"
|
|
bad_mounts := [mount.destination |
|
|
mount := input.mounts[_]
|
|
not mount_matches(mount)
|
|
]
|
|
|
|
count(bad_mounts) > 0
|
|
mountError := concat(" ", ["invalid mount list:", concat(",", bad_mounts)])
|
|
}
|
|
|
|
default signal_allowed := false
|
|
|
|
signal_allowed {
|
|
some container in data.metadata.matches[input.containerID]
|
|
signal_ok(container.signals)
|
|
}
|
|
|
|
signal_allowed {
|
|
some container in data.metadata.matches[input.containerID]
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
signal_ok(process.signals)
|
|
}
|
|
|
|
errors["target isn't allowed to receive the signal"] {
|
|
input.rule == "signal_container_process"
|
|
not signal_allowed
|
|
}
|
|
|
|
errors["device already mounted at path"] {
|
|
input.rule == "plan9_mount"
|
|
plan9_mounted(input.target)
|
|
}
|
|
|
|
errors["no device at path to unmount"] {
|
|
input.rule == "plan9_unmount"
|
|
not plan9_mounted(input.unmountTarget)
|
|
}
|
|
|
|
default fragment_issuer_matches := false
|
|
|
|
fragment_issuer_matches {
|
|
some fragment in candidate_fragments
|
|
fragment.issuer == input.issuer
|
|
}
|
|
|
|
errors["invalid fragment issuer"] {
|
|
input.rule == "load_fragment"
|
|
not fragment_issuer_matches
|
|
}
|
|
|
|
default fragment_feed_matches := false
|
|
|
|
fragment_feed_matches {
|
|
some fragment in candidate_fragments
|
|
fragment.issuer == input.issuer
|
|
fragment.feed == input.feed
|
|
}
|
|
|
|
fragment_feed_matches {
|
|
input.feed in data.metadata.issuers[input.issuer]
|
|
}
|
|
|
|
errors["invalid fragment feed"] {
|
|
input.rule == "load_fragment"
|
|
fragment_issuer_matches
|
|
not fragment_feed_matches
|
|
}
|
|
|
|
default fragment_version_is_valid := false
|
|
|
|
fragment_version_is_valid {
|
|
some fragment in candidate_fragments
|
|
fragment.issuer == input.issuer
|
|
fragment.feed == input.feed
|
|
svn_ok(data[input.namespace].svn, fragment.minimum_svn)
|
|
}
|
|
|
|
default svn_mismatch := false
|
|
|
|
svn_mismatch {
|
|
some fragment in candidate_fragments
|
|
fragment.issuer == input.issuer
|
|
fragment.feed == input.feed
|
|
to_number(data[input.namespace].svn)
|
|
semver.is_valid(fragment.minimum_svn)
|
|
}
|
|
|
|
svn_mismatch {
|
|
some fragment in candidate_fragments
|
|
fragment.issuer == input.issuer
|
|
fragment.feed == input.feed
|
|
semver.is_valid(data[input.namespace].svn)
|
|
to_number(fragment.minimum_svn)
|
|
}
|
|
|
|
errors["fragment svn is below the specified minimum"] {
|
|
input.rule == "load_fragment"
|
|
fragment_feed_matches
|
|
not svn_mismatch
|
|
not fragment_version_is_valid
|
|
}
|
|
|
|
errors["fragment svn and the specified minimum are different types"] {
|
|
input.rule == "load_fragment"
|
|
fragment_feed_matches
|
|
svn_mismatch
|
|
}
|
|
|
|
errors["scratch already mounted at path"] {
|
|
input.rule == "scratch_mount"
|
|
scratch_mounted(input.target)
|
|
}
|
|
|
|
errors["unencrypted scratch not allowed"] {
|
|
input.rule == "scratch_mount"
|
|
not allow_unencrypted_scratch
|
|
not input.encrypted
|
|
}
|
|
|
|
errors["no scratch at path to unmount"] {
|
|
input.rule == "scratch_unmount"
|
|
not scratch_mounted(input.unmountTarget)
|
|
}
|
|
|
|
errors[framework_version_error] {
|
|
policy_framework_version == null
|
|
framework_version_error := concat(" ", ["framework_version is missing. Current version:", version])
|
|
}
|
|
|
|
errors[framework_version_error] {
|
|
semver.compare(policy_framework_version, version) > 0
|
|
framework_version_error := concat(" ", ["framework_version is ahead of the current version:", policy_framework_version, "is greater than", version])
|
|
}
|
|
|
|
errors[fragment_framework_version_error] {
|
|
input.namespace
|
|
fragment_framework_version == null
|
|
fragment_framework_version_error := concat(" ", ["fragment framework_version is missing. Current version:", version])
|
|
}
|
|
|
|
errors[fragment_framework_version_error] {
|
|
input.namespace
|
|
semver.compare(fragment_framework_version, version) > 0
|
|
fragment_framework_version_error := concat(" ", ["fragment framework_version is ahead of the current version:", fragment_framework_version, "is greater than", version])
|
|
}
|
|
|
|
errors["containers only distinguishable by allow_stdio_access"] {
|
|
input.rule == "create_container"
|
|
|
|
not container_started
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
privileged_ok(container.allow_elevated)
|
|
workingDirectory_ok(container.working_dir)
|
|
command_ok(container.command)
|
|
mountList_ok(container.mounts, container.allow_elevated)
|
|
seccomp_ok(container.seccomp_profile_sha256)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
# check to see if the capabilities variables match, dropping
|
|
# them if allowed (and necessary)
|
|
caps_list := valid_caps_for_all(possible_after_env_containers, input.privileged)
|
|
possible_after_caps_containers := [container |
|
|
container := possible_after_env_containers[_]
|
|
caps_ok(get_capabilities(container, input.privileged), caps_list)
|
|
]
|
|
|
|
count(possible_after_caps_containers) > 0
|
|
|
|
# set final container list
|
|
containers := possible_after_caps_containers
|
|
|
|
allow_stdio_access := containers[0].allow_stdio_access
|
|
some c in containers
|
|
c.allow_stdio_access != allow_stdio_access
|
|
}
|
|
|
|
errors["external processes only distinguishable by allow_stdio_access"] {
|
|
input.rule == "exec_external"
|
|
|
|
possible_processes := [process |
|
|
process := candidate_external_processes[_]
|
|
workingDirectory_ok(process.working_dir)
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_processes) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_processes)
|
|
processes := [process |
|
|
process := possible_processes[_]
|
|
envList_ok(process.env_rules, env_list)
|
|
]
|
|
|
|
count(processes) > 0
|
|
|
|
allow_stdio_access := processes[0].allow_stdio_access
|
|
some p in processes
|
|
p.allow_stdio_access != allow_stdio_access
|
|
}
|
|
|
|
|
|
default noNewPrivileges_matches := false
|
|
|
|
noNewPrivileges_matches {
|
|
input.rule == "create_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
}
|
|
|
|
noNewPrivileges_matches {
|
|
input.rule == "exec_in_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
workingDirectory_ok(process.working_dir)
|
|
noNewPrivileges_ok(process.no_new_privileges)
|
|
}
|
|
|
|
errors["invalid noNewPrivileges"] {
|
|
input.rule in ["create_container", "exec_in_container"]
|
|
not noNewPrivileges_matches
|
|
}
|
|
|
|
default user_matches := false
|
|
|
|
user_matches {
|
|
input.rule == "create_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
user_ok(container.user)
|
|
}
|
|
|
|
user_matches {
|
|
input.rule == "exec_in_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
workingDirectory_ok(process.working_dir)
|
|
user_ok(process.user)
|
|
}
|
|
|
|
errors["invalid user"] {
|
|
input.rule in ["create_container", "exec_in_container"]
|
|
not user_matches
|
|
}
|
|
|
|
errors["capabilities don't match"] {
|
|
input.rule == "create_container"
|
|
|
|
not container_started
|
|
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
privileged_ok(container.allow_elevated)
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
workingDirectory_ok(container.working_dir)
|
|
command_ok(container.command)
|
|
mountList_ok(container.mounts, container.allow_elevated)
|
|
seccomp_ok(container.seccomp_profile_sha256)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
# check to see if the capabilities variables match, dropping
|
|
# them if allowed (and necessary)
|
|
caps_list := valid_caps_for_all(possible_after_env_containers, input.privileged)
|
|
possible_after_caps_containers := [container |
|
|
container := possible_after_env_containers[_]
|
|
caps_ok(get_capabilities(container, input.privileged), caps_list)
|
|
]
|
|
|
|
count(possible_after_caps_containers) == 0
|
|
}
|
|
|
|
errors["capabilities don't match"] {
|
|
input.rule == "exec_in_container"
|
|
|
|
container_started
|
|
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
workingDirectory_ok(container.working_dir)
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
some process in container.exec_processes
|
|
command_ok(process.command)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
# check to see if the capabilities variables match, dropping
|
|
# them if allowed (and necessary)
|
|
caps_list := valid_caps_for_all(possible_after_env_containers, container_privileged)
|
|
possible_after_caps_containers := [container |
|
|
container := possible_after_env_containers[_]
|
|
caps_ok(get_capabilities(container, container_privileged), caps_list)
|
|
]
|
|
|
|
count(possible_after_caps_containers) == 0
|
|
}
|
|
|
|
# covers exec_in_container as well. it shouldn't be possible to ever get
|
|
# an exec_in_container as it "inherits" capabilities rules from create_container
|
|
errors["containers only distinguishable by capabilties"] {
|
|
input.rule == "create_container"
|
|
|
|
allow_capability_dropping
|
|
not container_started
|
|
|
|
# narrow the matches based upon command, working directory, and
|
|
# mount list
|
|
possible_after_initial_containers := [container |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
# NB any change to these narrowing conditions should be reflected in
|
|
# the error handling, such that error messaging correctly reflects
|
|
# the narrowing process.
|
|
noNewPrivileges_ok(container.no_new_privileges)
|
|
user_ok(container.user)
|
|
privileged_ok(container.allow_elevated)
|
|
workingDirectory_ok(container.working_dir)
|
|
command_ok(container.command)
|
|
mountList_ok(container.mounts, container.allow_elevated)
|
|
]
|
|
|
|
count(possible_after_initial_containers) > 0
|
|
|
|
# check to see if the environment variables match, dropping
|
|
# them if allowed (and necessary)
|
|
env_list := valid_envs_for_all(possible_after_initial_containers)
|
|
possible_after_env_containers := [container |
|
|
container := possible_after_initial_containers[_]
|
|
envList_ok(container.env_rules, env_list)
|
|
]
|
|
|
|
count(possible_after_env_containers) > 0
|
|
|
|
largest := largest_caps_sets_for_all(possible_after_env_containers, input.privileged)
|
|
not all_caps_sets_are_equal(largest)
|
|
}
|
|
|
|
default seccomp_matches := false
|
|
|
|
seccomp_matches {
|
|
input.rule == "create_container"
|
|
some container in data.metadata.matches[input.containerID]
|
|
seccomp_ok(container.seccomp_profile_sha256)
|
|
}
|
|
|
|
errors["invalid seccomp"] {
|
|
input.rule == "create_container"
|
|
not seccomp_matches
|
|
}
|
|
|
|
default error_objects := null
|
|
|
|
error_objects := containers {
|
|
input.rule == "create_container"
|
|
containers := data.metadata.matches[input.containerID]
|
|
}
|
|
|
|
error_objects := processes {
|
|
input.rule == "exec_in_container"
|
|
processes := [process |
|
|
container := data.metadata.matches[input.containerID][_]
|
|
process := container.exec_processes[_]
|
|
]
|
|
}
|
|
|
|
error_objects := processes {
|
|
input.rule == "exec_external"
|
|
processes := candidate_external_processes
|
|
}
|
|
|
|
error_objects := fragments {
|
|
input.rule == "load_fragment"
|
|
fragments := candidate_fragments
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Logic for providing backwards compatibility for framework data objects
|
|
################################################################################
|
|
|
|
|
|
check_container(raw_container, framework_version) := container {
|
|
semver.compare(framework_version, version) == 0
|
|
container := raw_container
|
|
}
|
|
|
|
check_container(raw_container, framework_version) := container {
|
|
semver.compare(framework_version, version) < 0
|
|
container := {
|
|
# Base fields
|
|
"command": raw_container.command,
|
|
"env_rules": raw_container.env_rules,
|
|
"layers": raw_container.layers,
|
|
"mounts": raw_container.mounts,
|
|
"allow_elevated": raw_container.allow_elevated,
|
|
"working_dir": raw_container.working_dir,
|
|
"exec_processes": raw_container.exec_processes,
|
|
"signals": raw_container.signals,
|
|
"allow_stdio_access": raw_container.allow_stdio_access,
|
|
# Additional fields need to have default logic applied
|
|
"no_new_privileges": check_no_new_privileges(raw_container, framework_version),
|
|
"user": check_user(raw_container, framework_version),
|
|
"capabilities": check_capabilities(raw_container, framework_version),
|
|
"seccomp_profile_sha256": check_seccomp_profile_sha256(raw_container, framework_version),
|
|
}
|
|
}
|
|
|
|
check_no_new_privileges(raw_container, framework_version) := no_new_privileges {
|
|
semver.compare(framework_version, "0.2.0") >= 0
|
|
no_new_privileges := raw_container.no_new_privileges
|
|
}
|
|
|
|
check_no_new_privileges(raw_container, framework_version) := no_new_privileges {
|
|
semver.compare(framework_version, "0.2.0") < 0
|
|
no_new_privileges := false
|
|
}
|
|
|
|
check_user(raw_container, framework_version) := user {
|
|
semver.compare(framework_version, "0.2.1") >= 0
|
|
user := raw_container.user
|
|
}
|
|
|
|
check_user(raw_container, framework_version) := user {
|
|
semver.compare(framework_version, "0.2.1") < 0
|
|
user := {
|
|
"umask": "0022",
|
|
"user_idname": {
|
|
"pattern": "",
|
|
"strategy": "any"
|
|
},
|
|
"group_idnames": [
|
|
{
|
|
"pattern": "",
|
|
"strategy": "any"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
check_capabilities(raw_container, framework_version) := capabilities {
|
|
semver.compare(framework_version, "0.2.2") >= 0
|
|
capabilities := raw_container.capabilities
|
|
}
|
|
|
|
check_capabilities(raw_container, framework_version) := capabilities {
|
|
semver.compare(framework_version, "0.2.2") < 0
|
|
# we cannot determine a reasonable default at the time this is called,
|
|
# which is either during `mount_overlay` or `load_fragment`, and so
|
|
# we set it to `null`, which indicates that the capabilities should
|
|
# be determined dynamically when needed.
|
|
capabilities := null
|
|
}
|
|
|
|
check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 {
|
|
semver.compare(framework_version, "0.2.3") >= 0
|
|
seccomp_profile_sha256 := raw_container.seccomp_profile_sha256
|
|
}
|
|
|
|
check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 {
|
|
semver.compare(framework_version, "0.2.3") < 0
|
|
seccomp_profile_sha256 := ""
|
|
}
|
|
|
|
check_external_process(raw_process, framework_version) := process {
|
|
semver.compare(framework_version, version) == 0
|
|
process := raw_process
|
|
}
|
|
|
|
check_external_process(raw_process, framework_version) := process {
|
|
semver.compare(framework_version, version) < 0
|
|
process := {
|
|
# Base fields
|
|
"command": raw_process.command,
|
|
"env_rules": raw_process.env_rules,
|
|
"working_dir": raw_process.working_dir,
|
|
"allow_stdio_access": raw_process.allow_stdio_access,
|
|
# Additional fields need to have default logic applied
|
|
}
|
|
}
|
|
|
|
check_fragment(raw_fragment, framework_version) := fragment {
|
|
semver.compare(framework_version, version) == 0
|
|
fragment := raw_fragment
|
|
}
|
|
|
|
check_fragment(raw_fragment, framework_version) := fragment {
|
|
semver.compare(framework_version, version) < 0
|
|
fragment := {
|
|
# Base fields
|
|
"issuer": raw_fragment.issuer,
|
|
"feed": raw_fragment.feed,
|
|
"minimum_svn": raw_fragment.minimum_svn,
|
|
"includes": raw_fragment.includes,
|
|
# Additional fields need to have default logic applied
|
|
}
|
|
}
|
|
|
|
# base policy-level flags
|
|
allow_properties_access := data.policy.allow_properties_access
|
|
allow_dump_stacks := data.policy.allow_dump_stacks
|
|
allow_runtime_logging := data.policy.allow_runtime_logging
|
|
allow_environment_variable_dropping := data.policy.allow_environment_variable_dropping
|
|
allow_unencrypted_scratch := data.policy.allow_unencrypted_scratch
|
|
|
|
# all flags not in the base set need to have default logic applied
|
|
|
|
default allow_capability_dropping := false
|
|
|
|
allow_capability_dropping := flag {
|
|
semver.compare(policy_framework_version, "0.2.2") >= 0
|
|
flag := data.policy.allow_capability_dropping
|
|
}
|
|
|
|
default policy_framework_version := null
|
|
default policy_api_version := null
|
|
|
|
policy_framework_version := data.policy.framework_version
|
|
policy_api_version := data.policy.api_version
|
|
|
|
# deprecated
|
|
policy_framework_version := data.policy.framework_svn
|
|
policy_api_version := data.policy.api_svn
|
|
fragment_framework_version := data[input.namespace].framework_svn
|