From bb0ca2975324bf7c3207800ce3a703231b843dec Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:32:11 -0800 Subject: [PATCH] Lock down ACI tests and more OPA test folders (#54) 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 --- Cargo.toml | 6 + src/builtins/deprecated.rs | 2 + src/builtins/mod.rs | 3 +- src/builtins/regex.rs | 51 + src/interpreter.rs | 196 ++-- src/lexer.rs | 2 +- src/scheduler.rs | 86 +- tests/aci/aci.yaml | 390 ++++++++ tests/aci/api.rego | 26 + tests/aci/framework.rego | 1831 ++++++++++++++++++++++++++++++++++++ tests/aci/main.rs | 120 +++ tests/aci/policy.rego | 89 ++ tests/opa.passing | 6 + tests/opa.rs | 7 +- 14 files changed, 2712 insertions(+), 103 deletions(-) create mode 100644 src/builtins/regex.rs create mode 100644 tests/aci/aci.yaml create mode 100644 tests/aci/api.rego create mode 100644 tests/aci/framework.rego create mode 100644 tests/aci/main.rs create mode 100644 tests/aci/policy.rego diff --git a/Cargo.toml b/Cargo.toml index dfe9135..5f30aad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ env_logger="0.10.0" lazy_static = "1.4.0" rand = "0.8.5" data-encoding = "2.4.0" +regex = "1.10.2" [dev-dependencies] clap = { version = "4.4.7", features = ["derive"] } @@ -33,3 +34,8 @@ debug = true name="opa" harness=false test=false + +[[test]] +name="aci" +harness=false +test=false diff --git a/src/builtins/deprecated.rs b/src/builtins/deprecated.rs index 81b5be9..83889ba 100644 --- a/src/builtins/deprecated.rs +++ b/src/builtins/deprecated.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::ast::{Expr, Ref}; +use crate::builtins::regex::regex_match; use crate::builtins::utils::{ensure_args_count, ensure_set}; use crate::builtins::BuiltinFcn; use crate::lexer::Span; @@ -20,6 +21,7 @@ lazy_static! { m.insert("all", (all, 1)); m.insert("any", (any, 1)); m.insert("set_diff", (set_diff, 2)); + m.insert("re_match", (regex_match, 2)); m }; } diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 8ac2d14..121af01 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -11,6 +11,7 @@ pub mod deprecated; mod encoding; pub mod numbers; mod objects; +mod regex; mod semver; pub mod sets; mod strings; @@ -45,7 +46,7 @@ lazy_static! { sets::register(&mut m); objects::register(&mut m); strings::register(&mut m); - //regex::register(&mut m); + regex::register(&mut m); //glob::register(&mut m); bitwise::register(&mut m); conversions::register(&mut m); diff --git a/src/builtins/regex.rs b/src/builtins/regex.rs new file mode 100644 index 0000000..110b5a9 --- /dev/null +++ b/src/builtins/regex.rs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::ast::{Expr, Ref}; +use crate::builtins; +use crate::builtins::utils::{ensure_args_count, ensure_string}; +use crate::lexer::Span; +use crate::value::Value; + +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use regex::Regex; + +pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("regex.is_valid", (is_valid, 1)); + m.insert("regex.match", (regex_match, 2)); + m.insert("regex.split", (regex_split, 2)); +} + +fn is_valid(span: &Span, params: &[Ref], args: &[Value]) -> Result { + let name = "regex.is_valid"; + ensure_args_count(span, name, params, args, 1)?; + Ok(ensure_string(name, ¶ms[0], &args[0]) + .map_or(Value::Bool(false), |p| Value::Bool(Regex::new(&p).is_ok()))) +} + +pub fn regex_match(span: &Span, params: &[Ref], args: &[Value]) -> Result { + let name = "regex.match"; + ensure_args_count(span, name, params, args, 2)?; + let pattern = ensure_string(name, ¶ms[0], &args[0])?; + let value = ensure_string(name, ¶ms[1], &args[1])?; + + let pattern = Regex::new(&pattern).or_else(|_| bail!(span.error("invalid regex")))?; + Ok(Value::Bool(pattern.is_match(&value))) +} + +pub fn regex_split(span: &Span, params: &[Ref], args: &[Value]) -> Result { + let name = "regex.split"; + ensure_args_count(span, name, params, args, 2)?; + let pattern = ensure_string(name, ¶ms[0], &args[0])?; + let value = ensure_string(name, ¶ms[1], &args[1])?; + + let pattern = Regex::new(&pattern).or_else(|_| bail!(span.error("invalid regex")))?; + Ok(Value::from_array( + pattern + .split(&value) + .map(|s| Value::String(s.into())) + .collect::>(), + )) +} diff --git a/src/interpreter.rs b/src/interpreter.rs index 94b4084..09797ad 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -242,7 +242,7 @@ impl Interpreter { // Stop path collection upon encountering the leading variable. Expr::Var(v) => { path.reverse(); - return self.lookup_var(v, &path[..]); + return self.lookup_var(v, &path[..], false); } // Accumulate chained . field accesses. Expr::RefDot { refr, field, .. } => { @@ -259,8 +259,23 @@ impl Interpreter { // Note, we have the choice to evaluate a non-string index _ => { path.reverse(); - let obj = self.eval_expr(refr)?; + let index = self.eval_expr(index)?; + + // Handle indexing into data. + if let Ok(ref_path) = get_path_string(refr, None) { + if get_root_var(refr)?.text() == "data" && index != Value::Undefined { + let index = match &index { + Value::String(s) => s.to_string(), + _ => index.to_string(), + }; + let ref_path = ref_path + "." + &index + "." + &path.join(""); + self.ensure_rule_evaluated(ref_path)?; + } + } + + let obj = self.eval_expr(refr)?; + let mut v = obj[&index].clone(); // Qualified references starting with data (e.g data.p.q) can // be indexed using numbers. The number will be converted to string @@ -451,55 +466,87 @@ impl Interpreter { let (name, value) = match op { AssignOp::Eq => { match (lhs.as_ref(), rhs.as_ref()) { - (Expr::Var(lhs_span), Expr::Var(rhs_span)) => { - let (lhs_name, lhs_var) = (lhs_span.source_str(), self.eval_expr(lhs)?); - let (rhs_name, rhs_var) = (rhs_span.source_str(), self.eval_expr(rhs)?); - - match (&lhs_var, &rhs_var) { - (Value::Undefined, Value::Undefined) => { - bail!(lhs.span().error("both operands are unsafe")) + (_, Expr::Var(var)) if self.lookup_var(var, &[], true)? == Value::Undefined => { + (var.source_str(), self.eval_expr(lhs)?) + } + (Expr::Var(var), _) if self.lookup_var(var, &[], true)? == Value::Undefined => { + (var.source_str(), self.eval_expr(rhs)?) + } + ( + Expr::Array { + items: lhs_items, .. + }, + Expr::Array { + items: rhs_items, + span: rhs_span, + }, + ) => { + if lhs_items.len() != rhs_items.len() { + bail!(rhs_span + .error("mismatch in number of array elements in lhs and rhs")); + } + for (lhs, rhs) in std::iter::zip(lhs_items.iter(), rhs_items.iter()) { + if self.eval_assign_expr(&AssignOp::Eq, lhs, rhs)? != Value::Bool(true) + { + return Ok(Value::Bool(false)); } - (Value::Undefined, _) => (lhs_name, rhs_var), - (_, Value::Undefined) => (rhs_name, lhs_var), - // TODO: avoid reeval - _ => return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs), } + return Ok(Value::Bool(true)); } - (Expr::Var(lhs_span), _) => { - let (name, var) = (lhs_span.source_str(), self.eval_expr(lhs)?); - - // TODO: Check this - // Allow variable overwritten inside a loop - if !matches!(var, Value::Undefined) - && self.loop_var_values.get(rhs).is_none() - { - return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs); - } - - (name, self.eval_expr(rhs)?) + (Expr::Object { .. }, Expr::Object { .. }) => { + // TODO: destructure + return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs); } - (_, Expr::Var(rhs_span)) => { - let (name, var) = (rhs_span.source_str(), self.eval_expr(rhs)?); - - // TODO: Check this - // Allow variable overwritten inside a loop - if !matches!(var, Value::Undefined) - && self.loop_var_values.get(lhs).is_none() - { - return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs); - } - - (name, self.eval_expr(lhs)?) + (Expr::Array { .. }, _) => { + let value = self.eval_expr(rhs)?; + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + return self + .make_bindings(false, &mut type_match, &mut cache, lhs, &value) + .map(Value::Bool); + } + (_, Expr::Array { .. }) => { + let value = self.eval_expr(lhs)?; + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + return self + .make_bindings(false, &mut type_match, &mut cache, rhs, &value) + .map(Value::Bool); + } + (Expr::Object { .. }, _) => { + let value = self.eval_expr(rhs)?; + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + return self + .make_bindings(false, &mut type_match, &mut cache, lhs, &value) + .map(Value::Bool); + } + (_, Expr::Object { .. }) => { + let value = self.eval_expr(lhs)?; + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + return self + .make_bindings(false, &mut type_match, &mut cache, rhs, &value) + .map(Value::Bool); } // Treat the assignment as comparison if neither lhs nor rhs is a variable _ => return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs), } } AssignOp::ColEq => { + let rhs_value = self.eval_expr(rhs)?; + if rhs_value == Value::Undefined { + return Ok(rhs_value); + } + let name = if let Expr::Var(span) = lhs.as_ref() { span.source_str() } else { - bail!("internal error: unexpected"); + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + return self + .make_bindings(false, &mut type_match, &mut cache, lhs, &rhs_value) + .map(Value::Bool); }; // TODO: Check this @@ -513,7 +560,7 @@ impl Interpreter { .error(&format!("redefinition for variable {}", name))); } - (name, self.eval_expr(rhs)?) + (name, rhs_value) } }; @@ -632,6 +679,7 @@ impl Interpreter { let raise_error = is_last && type_match.get(expr).is_none(); match (expr.as_ref(), value) { + (Expr::Var(ident), _) if ident.text().as_ref() == &"_" => Ok(true), (Expr::Var(ident), _) => { self.add_variable(&ident.source_str(), value.clone())?; Ok(true) @@ -1649,29 +1697,25 @@ impl Interpreter { )); } - let mut args_scope = Scope::new(); - for (idx, a) in args.iter().enumerate() { - let a = match a.as_ref() { - Expr::Var(s) => s.source_str(), - _ => { - match self.eval_expr(a) { - Ok(a) => { - if a != param_values[idx] { - // Skip this rule definition. - continue 'outer; - } + // Back up local variables of current function and empty + // the local variables of callee function. + let scopes = std::mem::take(&mut self.scopes); - continue; - } - _ => { - // TODO: destructuring function arguments. - continue; - } - } - } - }; - //TODO: check call in params - args_scope.insert(a, param_values[idx].clone()); + // Set the arguments scope. + let args_scope = Scope::new(); + self.scopes.push(args_scope); + + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + + for (idx, a) in args.iter().enumerate() { + if self + .make_bindings(false, &mut type_match, &mut cache, a, ¶m_values[idx]) + .is_err() + { + self.scopes = scopes; + continue 'outer; + } } let ctx = Context { @@ -1683,12 +1727,6 @@ impl Interpreter { is_compr: false, }; - // Back up local variables of current function and empty - // the local variables of callee function. - let scopes = std::mem::take(&mut self.scopes); - - // Set the arguments scope. - self.scopes.push(args_scope); let value = match self.eval_rule_bodies(ctx, span, bodies) { Ok(v) => v, Err(e) => { @@ -1768,10 +1806,17 @@ impl Interpreter { } Ok(Value::Bool(true)) } - _ => { + _ if allow_return_arg => { let ret_value = self.eval_call_impl(span, fcn, ¶ms[..params.len() - 1])?; - let value = self.eval_expr(&ea)?; - Ok(Value::Bool(ret_value == value)) + let mut cache = BTreeMap::new(); + let mut type_match = BTreeSet::new(); + self.make_bindings(false, &mut type_match, &mut cache, &ea, &ret_value) + .map(Value::Bool) + } + _ => { + let expected = self.eval_expr(¶ms[params.len() - 1])?; + let ret_value = self.eval_call_impl(span, fcn, ¶ms[..params.len() - 1])?; + Ok(Value::Bool(ret_value == expected)) } } } else { @@ -1813,7 +1858,7 @@ impl Interpreter { Ok(()) } - fn lookup_var(&mut self, span: &Span, fields: &[&str]) -> Result { + fn lookup_var(&mut self, span: &Span, fields: &[&str], no_error: bool) -> Result { let name = span.source_str(); // Return local variable/argument. @@ -1828,6 +1873,9 @@ impl Interpreter { // TODO: should we return before checking for input? if self.no_rules_lookup { + if no_error { + return Ok(Value::Undefined); + } return Err(span.error("undefined var")); } @@ -1869,6 +1917,12 @@ impl Interpreter { Self::get_path_string(&self.current_module()?.package.refr, Some("data"))?; let rule_path = module_path + "." + name.text(); + if !no_error + && self.rules.get(&rule_path).is_none() + && self.default_rules.get(&rule_path).is_none() + { + bail!(span.error("var is unsafe")); + } self.ensure_rule_evaluated(rule_path)?; let value = Self::get_value_chained(self.data.clone(), &path[..]); diff --git a/src/lexer.rs b/src/lexer.rs index aad4232..35be524 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -147,7 +147,7 @@ impl Source { let col_spaces = col as usize - 1; format!( - "\n-->{}:{}:{}\n{: {}:{}:{}\n{: { @@ -600,19 +609,26 @@ impl Analyzer { scope: &mut Scope, first_use: &mut BTreeMap, definitions: &mut Vec>, - return_arg: &Option>, + assigned_vars: &Option<&BTreeSet>, ) -> Result<(Vec, Vec>)> { let mut used_vars = vec![]; let mut comprs = vec![]; traverse(expr, &mut |e| match e.as_ref() { Var(v) if !matches!(*v.text(), "_" | "input" | "data") => { let name = v.source_str(); + let is_extra_arg = match assigned_vars { + Some(vars) => vars.contains(&v.source_str()), + _ => false, + }; + if scope.locals.contains(&name) /*|| scope.inputs.contains(name) */ { - used_vars.push(name.clone()); - first_use.entry(name).or_insert(v.clone()); - } else if !scope.inputs.contains(&name) && Some(e.clone()) != *return_arg { + if !is_extra_arg { + used_vars.push(name.clone()); + first_use.entry(name).or_insert(v.clone()); + } + } else if !scope.inputs.contains(&name) { bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str())); } Ok(false) @@ -627,7 +643,7 @@ impl Analyzer { scope, first_use, definitions, - return_arg, + assigned_vars, )?; definitions.push(Definition { var: var.clone(), @@ -992,30 +1008,42 @@ impl Analyzer { &self.functions, ); if let Some(ref ea) = extra_arg { - if let Expr::Var(return_arg) = ea.as_ref() { - let (mut used_vars, comprs) = Self::gather_used_vars_comprs_index_vars( - expr, - &mut scope, - &mut first_use, - &mut definitions, - &extra_arg, - )?; - let var = if *return_arg.text() != "_" { - // The var in the return argument slot would have been processed as - // an used var. Remove it from used vars and add it as the variable being - // defined. - used_vars.pop(); - return_arg.source_str() - } else { - empty_str.clone() - }; - self.process_comprs( - &comprs[..], - &mut scope, - &mut first_use, - &mut used_vars, - )?; - definitions.push(Definition { var, used_vars }); + // Gather vars that are being bound + let mut extras_scope = Scope::default(); + gather_assigned_vars(ea, false, &self.scopes, &mut extras_scope)?; + + for var in &extras_scope.locals { + scope.locals.insert(var.clone()); + } + + // Gather vars being used. + let (mut used_vars, comprs) = Self::gather_used_vars_comprs_index_vars( + expr, + &mut scope, + &mut first_use, + &mut definitions, + &Some(&extras_scope.locals), + )?; + + self.process_comprs( + &comprs[..], + &mut scope, + &mut first_use, + &mut used_vars, + )?; + + if !extras_scope.locals.is_empty() { + for var in extras_scope.locals { + definitions.push(Definition { + var, + used_vars: used_vars.clone(), + }); + } + } else { + definitions.push(Definition { + var: empty_str.clone(), + used_vars, + }); } } else { self.process_expr(expr, &mut scope, &mut first_use, &mut definitions)?; diff --git a/tests/aci/aci.yaml b/tests/aci/aci.yaml new file mode 100644 index 0000000..b16e64c --- /dev/null +++ b/tests/aci/aci.yaml @@ -0,0 +1,390 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +cases: +- note: aci/mount_device + modules: + - api.rego + - framework.rego + - policy.rego + input: + deviceHash: 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + target: /run/layers/p0-layer0 + data: + metadata: {} + query: data.policy.mount_device=x + want_result: + - x: + allowed: true + metadata: + - action: add + key: /run/layers/p0-layer0 + name: devices + value: 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 +- note: aci/mount_overlay + modules: + - api.rego + - framework.rego + - policy.rego + input: + containerID: container0 + layerPaths: + - /run/layers/p0-layer0 + - /run/layers/p0-layer1 + - /run/layers/p0-layer2 + - /run/layers/p0-layer3 + - /run/layers/p0-layer4 + - /run/layers/p0-layer5 + target: /run/gcs/c/container0/rootfs + data: + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + "/run/layers/p0-layer1": e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c + "/run/layers/p0-layer2": eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79 + "/run/layers/p0-layer3": 41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156 + "/run/layers/p0-layer4": 4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c + "/run/layers/p0-layer5": fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a + query: data.policy.mount_overlay=x + want_result: + - x: + allowed: true + metadata: + - action: add + key: container0 + name: matches + value: + - {"allow_elevated":true,"allow_stdio_access":false,"capabilities":{"ambient":["CAP_SYS_ADMIN"],"bounding":["CAP_SYS_ADMIN"],"effective":["CAP_SYS_ADMIN"],"inheritable":["CAP_SYS_ADMIN"],"permitted":["CAP_SYS_ADMIN"]},"command":["rustc","--help"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"},{"pattern":"PREFIX_.+=.+","required":false,"strategy":"re2"}],"exec_processes":[{"command":["top"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[{"destination":"/container/path/one","options":["rbind","rshared","rw"],"source":"sandbox:///host/path/one","type":"bind"},{"destination":"/container/path/two","options":["rbind","rshared","ro"],"source":"sandbox:///host/path/two","type":"bind"}],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/user"} + - action: add + key: /run/gcs/c/container0/rootfs + name: overlayTargets + value: true +- note: aci/scratch_mount + modules: + - api.rego + - framework.rego + - policy.rego + input: + encrypted: true + target: /mnt/layer6 + data: + metadata: {} + query: data.policy.scratch_mount=x + want_result: + - x: + allowed: true + metadata: + - action: add + key: /mnt/layer6 + name: scratch_mounts + value: + encrypted: true +- note: aci/create_container + modules: + - api.rego + - framework.rego + - policy.rego + input: + argList: + - rustc + - --help + capabilities: + ambient: ["CAP_SYS_ADMIN"] + bounding: ["CAP_SYS_ADMIN"] + effective: ["CAP_SYS_ADMIN"] + inheritable: ["CAP_SYS_ADMIN"] + permitted: ["CAP_SYS_ADMIN"] + containerID: container0 + envList: + - CARGO_HOME=/usr/local/cargo + - RUST_VERSION=1.52.1 + - TERM=xterm + - PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - RUSTUP_HOME=/usr/local/rustup + groups: + - id: 0 + name: root + hugePagesDir: /run/gcs/c/sandbox0/hugepages + mounts: + - destination: /container/path/one + options: ["rbind", "rshared", "rw"] + source: /run/gcs/c/sandbox0/sandboxMounts/host/path/one + type: bind + - destination: /container/path/two + options: ["rbind", "rshared", "ro"] + source: /run/gcs/c/sandbox0/sandboxMounts/host/path/two + type: bind + noNewPrivileges: true + privileged: false + seccompProfileSHA256: "" + sandboxDir: /run/gcs/c/sandbox0/sandboxMounts + umask: "0022" + user: + id: 0 + name: root + workingDir: /home/user + data: + sandboxPrefix: "sandbox://" + hugePagesPrefix: "hugepages://" + plan9Prefix: "plan9://" + defaultMounts: [] + privilegedMounts: [] + defaultPrivilegedCapabilities: + - CAP_CHOWN + - CAP_DAC_OVERRIDE + - CAP_DAC_READ_SEARCH + - CAP_FOWNER + - CAP_FSETID + - CAP_KILL + - CAP_SETGID + - CAP_SETUID + - CAP_SETPCAP + - CAP_LINUX_IMMUTABLE + - CAP_NET_BIND_SERVICE + - CAP_NET_BROADCAST + - CAP_NET_ADMIN + - CAP_NET_RAW + - CAP_IPC_LOCK + - CAP_IPC_OWNER + - CAP_SYS_MODULE + - CAP_SYS_RAWIO + - CAP_SYS_CHROOT + - CAP_SYS_PTRACE + - CAP_SYS_PACCT + - CAP_SYS_ADMIN + - CAP_SYS_BOOT + - CAP_SYS_NICE + - CAP_SYS_RESOURCE + - CAP_SYS_TIME + - CAP_SYS_TTY_CONFIG + - CAP_MKNOD + - CAP_LEASE + - CAP_AUDIT_WRITE + - CAP_AUDIT_CONTROL + - CAP_SETFCAP + - CAP_MAC_OVERRIDE + - CAP_MAC_ADMIN + - CAP_SYSLOG + - CAP_WAKE_ALARM + - CAP_BLOCK_SUSPEND + - CAP_AUDIT_READ + - CAP_PERFMON + - CAP_BPF + - CAP_CHECKPOINT_RESTORE + defaultUnprivilegedCapabilities: + - CAP_DAC_OVERRIDE + - CAP_FSETID + - CAP_FOWNER + - CAP_MKNOD + - CAP_NET_RAW + - CAP_SETGID + - CAP_SETUID + - CAP_SETFCAP + - CAP_SETPCAP + - CAP_NET_BIND_SERVICE + - CAP_SYS_CHROOT + - CAP_KILL + - CAP_AUDIT_WRITE + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + "/run/layers/p0-layer1": e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c + "/run/layers/p0-layer2": eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79 + "/run/layers/p0-layer3": 41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156 + "/run/layers/p0-layer4": 4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c + "/run/layers/p0-layer5": fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a + matches: + container0: + - {"allow_elevated":true,"allow_stdio_access":false,"capabilities":{"ambient":["CAP_SYS_ADMIN"],"bounding":["CAP_SYS_ADMIN"],"effective":["CAP_SYS_ADMIN"],"inheritable":["CAP_SYS_ADMIN"],"permitted":["CAP_SYS_ADMIN"]},"command":["rustc","--help"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"},{"pattern":"PREFIX_.+=.+","required":false,"strategy":"re2"}],"exec_processes":[{"command":["top"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[{"destination":"/container/path/one","options":["rbind","rshared","rw"],"source":"sandbox:///host/path/one","type":"bind"},{"destination":"/container/path/two","options":["rbind","rshared","ro"],"source":"sandbox:///host/path/two","type":"bind"}],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/user"} + - {"allow_elevated":false,"allow_stdio_access":false,"capabilities":null,"command":["rustc","--version"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"}],"exec_processes":[{"command":["bash"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/fragment"} + overlayTargets: + "/run/gcs/c/container0/rootfs": true + scratch_mounts: + "/mnt/layer6": + encrypted: true + query: data.policy.create_container=x + want_result: + - x: + allow_stdio_access: false + allowed: true + caps_list: + ambient: ["CAP_SYS_ADMIN"] + bounding: ["CAP_SYS_ADMIN"] + effective: ["CAP_SYS_ADMIN"] + inheritable: ["CAP_SYS_ADMIN"] + permitted: ["CAP_SYS_ADMIN"] + env_list: + - CARGO_HOME=/usr/local/cargo + - RUST_VERSION=1.52.1 + - TERM=xterm + - PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - RUSTUP_HOME=/usr/local/rustup + metadata: + - action: update + key: container0 + name: matches + value: + - {"allow_elevated":true,"allow_stdio_access":false,"capabilities":{"ambient":["CAP_SYS_ADMIN"],"bounding":["CAP_SYS_ADMIN"],"effective":["CAP_SYS_ADMIN"],"inheritable":["CAP_SYS_ADMIN"],"permitted":["CAP_SYS_ADMIN"]},"command":["rustc","--help"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"},{"pattern":"PREFIX_.+=.+","required":false,"strategy":"re2"}],"exec_processes":[{"command":["top"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[{"destination":"/container/path/one","options":["rbind","rshared","rw"],"source":"sandbox:///host/path/one","type":"bind"},{"destination":"/container/path/two","options":["rbind","rshared","ro"],"source":"sandbox:///host/path/two","type":"bind"}],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/user"} + - action: add + key: container0 + name: started + value: + privileged: false +- note: aci/shutdown_container + modules: + - api.rego + - framework.rego + - policy.rego + input: + containerID: container0 + data: + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + "/run/layers/p0-layer1": e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c + "/run/layers/p0-layer2": eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79 + "/run/layers/p0-layer3": 41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156 + "/run/layers/p0-layer4": 4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c + "/run/layers/p0-layer5": fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a + matches: + container0: + - {"allow_elevated":true,"allow_stdio_access":false,"capabilities":{"ambient":["CAP_SYS_ADMIN"],"bounding":["CAP_SYS_ADMIN"],"effective":["CAP_SYS_ADMIN"],"inheritable":["CAP_SYS_ADMIN"],"permitted":["CAP_SYS_ADMIN"]},"command":["rustc","--help"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"},{"pattern":"PREFIX_.+=.+","required":false,"strategy":"re2"}],"exec_processes":[{"command":["top"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[{"destination":"/container/path/one","options":["rbind","rshared","rw"],"source":"sandbox:///host/path/one","type":"bind"},{"destination":"/container/path/two","options":["rbind","rshared","ro"],"source":"sandbox:///host/path/two","type":"bind"}],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/user"} + - {"allow_elevated":false,"allow_stdio_access":false,"capabilities":null,"command":["rustc","--version"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"}],"exec_processes":[{"command":["bash"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/fragment"} + overlayTargets: + "/run/gcs/c/container0/rootfs": true + scratch_mounts: + "/mnt/layer6": + encrypted: true + started: + container0: + - {"privileged": false} + query: data.policy.shutdown_container=x + want_result: + - x: + allowed: true + metadata: + - action: remove + key: container0 + name: matches +- note: aci/scratch_unmount + modules: + - api.rego + - framework.rego + - policy.rego + input: + unmountTarget: /mnt/layer6 + data: + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + "/run/layers/p0-layer1": e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c + "/run/layers/p0-layer2": eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79 + "/run/layers/p0-layer3": 41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156 + "/run/layers/p0-layer4": 4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c + "/run/layers/p0-layer5": fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a + overlayTargets: + "/run/gcs/c/container0/rootfs": true + scratch_mounts: + "/mnt/layer6": + encrypted: true + query: data.policy.scratch_unmount=x + want_result: + - x: + allowed: true + metadata: + - action: remove + key: /mnt/layer6 + name: scratch_mounts +- note: aci/unmount_overlay + modules: + - api.rego + - framework.rego + - policy.rego + input: + unmountTarget: /run/gcs/c/container0/rootfs + data: + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + "/run/layers/p0-layer1": e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c + "/run/layers/p0-layer2": eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79 + "/run/layers/p0-layer3": 41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156 + "/run/layers/p0-layer4": 4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c + "/run/layers/p0-layer5": fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a + overlayTargets: + "/run/gcs/c/container0/rootfs": true + scratch_mounts: [] + query: data.policy.unmount_overlay=x + want_result: + - x: + allowed: true + metadata: + - action: remove + key: /run/gcs/c/container0/rootfs + name: overlayTargets +- note: aci/unmount_device + modules: + - api.rego + - framework.rego + - policy.rego + input: + unmountTarget: /run/layers/p0-layer0 + data: + metadata: + devices: + "/run/layers/p0-layer0": 1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766 + query: data.policy.unmount_device=x + want_result: + - x: + allowed: true + metadata: + - action: remove + key: /run/layers/p0-layer0 + name: devices +- note: aci/load_fragment + modules: + - api.rego + - framework.rego + - policy.rego + - | + package fragment + + svn := "1" + framework_version := "0.3.0" + + containers := [ + { + "command": ["rustc","--version"], + "env_rules": [{"pattern": `PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, "strategy": "string", "required": true},{"pattern": `RUSTUP_HOME=/usr/local/rustup`, "strategy": "string", "required": true},{"pattern": `CARGO_HOME=/usr/local/cargo`, "strategy": "string", "required": true},{"pattern": `RUST_VERSION=1.52.1`, "strategy": "string", "required": true},{"pattern": `TERM=xterm`, "strategy": "string", "required": false}], + "layers": ["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"], + "mounts": [], + "exec_processes": [{"command": ["bash"], "signals": []}], + "signals": [], + "user": { + "user_idname": {"pattern": ``, "strategy": "any"}, + "group_idnames": [{"pattern": ``, "strategy": "any"}], + "umask": "0022" + }, + "capabilities": null, + "seccomp_profile_sha256": "", + "allow_elevated": false, + "working_dir": "/home/fragment", + "allow_stdio_access": false, + "no_new_privileges": true, + }, + ] + input: + feed: contoso.azurecr.io/infra + issuer: did:web:contoso.com + namespace: fragment + data: + metadata: {} + query: data.policy.load_fragment=x + want_result: + - x: + add_module: false + allowed: true + metadata: + - action: update + key: did:web:contoso.com + name: issuers + value: {"feeds":{"contoso.azurecr.io/infra":[{"containers":[{"allow_elevated":false,"allow_stdio_access":false,"capabilities":null,"command":["rustc","--version"],"env_rules":[{"pattern":"PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","required":true,"strategy":"string"},{"pattern":"RUSTUP_HOME=/usr/local/rustup","required":true,"strategy":"string"},{"pattern":"CARGO_HOME=/usr/local/cargo","required":true,"strategy":"string"},{"pattern":"RUST_VERSION=1.52.1","required":true,"strategy":"string"},{"pattern":"TERM=xterm","required":false,"strategy":"string"}],"exec_processes":[{"command":["bash"],"signals":[]}],"layers":["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"],"mounts":[],"no_new_privileges":true,"seccomp_profile_sha256":"","signals":[],"user":{"group_idnames":[{"pattern":"","strategy":"any"}],"umask":"0022","user_idname":{"pattern":"","strategy":"any"}},"working_dir":"/home/fragment"}]}]}} diff --git a/tests/aci/api.rego b/tests/aci/api.rego new file mode 100644 index 0000000..fa30144 --- /dev/null +++ b/tests/aci/api.rego @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +package api + +version := "0.10.0" + +enforcement_points := { + "mount_device": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}}, + "mount_overlay": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}}, + "create_container": {"introducedVersion": "0.1.0", "default_results": {"allowed": false, "env_list": null, "allow_stdio_access": false}}, + "unmount_device": {"introducedVersion": "0.2.0", "default_results": {"allowed": true}}, + "unmount_overlay": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}}, + "exec_in_container": {"introducedVersion": "0.2.0", "default_results": {"allowed": true, "env_list": null}}, + "exec_external": {"introducedVersion": "0.3.0", "default_results": {"allowed": true, "env_list": null, "allow_stdio_access": false}}, + "shutdown_container": {"introducedVersion": "0.4.0", "default_results": {"allowed": true}}, + "signal_container_process": {"introducedVersion": "0.5.0", "default_results": {"allowed": true}}, + "plan9_mount": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}}, + "plan9_unmount": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}}, + "get_properties": {"introducedVersion": "0.7.0", "default_results": {"allowed": true}}, + "dump_stacks": {"introducedVersion": "0.7.0", "default_results": {"allowed": true}}, + "runtime_logging": {"introducedVersion": "0.8.0", "default_results": {"allowed": true}}, + "load_fragment": {"introducedVersion": "0.9.0", "default_results": {"allowed": false, "add_module": false}}, + "scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}}, + "scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}}, +} diff --git a/tests/aci/framework.rego b/tests/aci/framework.rego new file mode 100644 index 0000000..0a7f65b --- /dev/null +++ b/tests/aci/framework.rego @@ -0,0 +1,1831 @@ +# 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 diff --git a/tests/aci/main.rs b/tests/aci/main.rs new file mode 100644 index 0000000..4cfc662 --- /dev/null +++ b/tests/aci/main.rs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +use regorus::*; + +use std::path::Path; +use std::time::Instant; + +use anyhow::Result; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use walkdir::WalkDir; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct TestCase { + note: String, + data: Value, + input: Value, + modules: Vec, + query: String, + want_result: Value, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct YamlTest { + cases: Vec, +} + +fn eval_test_case(dir: &Path, case: &TestCase) -> Result { + let mut engine = Engine::new(); + + engine.add_data(case.data.clone())?; + engine.set_input(case.input.clone()); + + for (idx, rego) in case.modules.iter().enumerate() { + if rego.ends_with(".rego") { + let path = dir.join(rego); + let path = path.to_str().expect("not a valid path"); + engine.add_policy_from_file(path.to_string())?; + } else { + engine.add_policy(format!("rego{idx}.rego"), rego.clone())?; + } + } + + let query_results = engine.eval_query(case.query.clone(), true)?; + + let mut values = vec![]; + for qr in query_results.result { + values.push(if !qr.bindings.is_empty_object() { + qr.bindings.clone() + } else if let Some(v) = qr.expressions.last() { + v["value"].clone() + } else { + Value::Undefined + }); + } + let result = Value::from_array(values); + // Make result json compatible. (E.g: avoid sets). + Value::from_json_str(&result.to_string()) +} + +fn run_aci_tests(dir: &Path) -> Result<()> { + let mut nfailures = 0; + for entry in WalkDir::new(dir) + .sort_by_file_name() + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if !path.to_string_lossy().ends_with(".yaml") { + continue; + } + + let yaml = std::fs::read(&path)?; + let yaml = String::from_utf8_lossy(&yaml); + let test: YamlTest = serde_yaml::from_str(&yaml)?; + + for case in &test.cases { + print!("{:50}", case.note); + let start = Instant::now(); + let results = eval_test_case(dir, case); + let duration = start.elapsed(); + + match results { + Ok(actual) if actual == case.want_result => { + println!("passed {:?}", duration); + } + Ok(actual) => { + println!("failed {:?}", duration); + println!("ACTUAL:"); + println!("{}", serde_json::to_string(&actual)?); + println!("EXPECTED"); + println!("{}", serde_json::to_string(&case.want_result)?); + nfailures += 1; + } + Err(e) => { + println!("failed {:?}", duration); + println!("{e}"); + nfailures += 1; + } + } + } + } + assert!(nfailures == 0); + + Ok(()) +} + +#[derive(clap::Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Path to ACI test suite. + #[arg(long, short)] + #[clap(default_value = "tests/aci")] + test_dir: String, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + run_aci_tests(&Path::new(&cli.test_dir)) +} diff --git a/tests/aci/policy.rego b/tests/aci/policy.rego new file mode 100644 index 0000000..068b5de --- /dev/null +++ b/tests/aci/policy.rego @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +package policy + +api_version := "0.10.0" +framework_version := "0.3.0" + +fragments := [ + {"issuer": "did:web:contoso.com", "feed": "contoso.azurecr.io/infra", "minimum_svn": "1", "includes": ["containers"]}, +] +containers := [ + { + "command": ["rustc","--help"], + "env_rules": [{"pattern": `PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, "strategy": "string", "required": true},{"pattern": `RUSTUP_HOME=/usr/local/rustup`, "strategy": "string", "required": true},{"pattern": `CARGO_HOME=/usr/local/cargo`, "strategy": "string", "required": true},{"pattern": `RUST_VERSION=1.52.1`, "strategy": "string", "required": true},{"pattern": `TERM=xterm`, "strategy": "string", "required": false},{"pattern": `PREFIX_.+=.+`, "strategy": "re2", "required": false}], + "layers": ["fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a","4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c","41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156","eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79","e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c","1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766"], + "mounts": [{"destination": "/container/path/one", "options": ["rbind","rshared","rw"], "source": "sandbox:///host/path/one", "type": "bind"},{"destination": "/container/path/two", "options": ["rbind","rshared","ro"], "source": "sandbox:///host/path/two", "type": "bind"}], + "exec_processes": [{"command": ["top"], "signals": []}], + "signals": [], + "user": { + "user_idname": {"pattern": ``, "strategy": "any"}, + "group_idnames": [{"pattern": ``, "strategy": "any"}], + "umask": "0022" + }, + "capabilities": { + "bounding": ["CAP_SYS_ADMIN"], + "effective": ["CAP_SYS_ADMIN"], + "inheritable": ["CAP_SYS_ADMIN"], + "permitted": ["CAP_SYS_ADMIN"], + "ambient": ["CAP_SYS_ADMIN"], + }, + "seccomp_profile_sha256": "", + "allow_elevated": true, + "working_dir": "/home/user", + "allow_stdio_access": false, + "no_new_privileges": true, + }, + { + "command": ["/pause"], + "env_rules": [{"pattern": `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, "strategy": "string", "required": true},{"pattern": `TERM=xterm`, "strategy": "string", "required": false}], + "layers": ["16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415"], + "mounts": [], + "exec_processes": [], + "signals": [], + "user": { + "user_idname": {"pattern": ``, "strategy": "any"}, + "group_idnames": [{"pattern": ``, "strategy": "any"}], + "umask": "0022" + }, + "capabilities": null, + "seccomp_profile_sha256": "", + "allow_elevated": false, + "working_dir": "/", + "allow_stdio_access": false, + "no_new_privileges": true, + }, +] +external_processes := [ + {"command": ["bash"], "env_rules": [{"pattern": `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, "strategy": "string", "required": true}], "working_dir": "/", "allow_stdio_access": false}, +] +allow_properties_access := false +allow_dump_stacks := false +allow_runtime_logging := false +allow_environment_variable_dropping := false +allow_unencrypted_scratch := false +allow_capability_dropping := true + + +mount_device := data.framework.mount_device +unmount_device := data.framework.unmount_device +mount_overlay := data.framework.mount_overlay +unmount_overlay := data.framework.unmount_overlay +create_container := data.framework.create_container +exec_in_container := data.framework.exec_in_container +exec_external := data.framework.exec_external +shutdown_container := data.framework.shutdown_container +signal_container_process := data.framework.signal_container_process +plan9_mount := data.framework.plan9_mount +plan9_unmount := data.framework.plan9_unmount +get_properties := data.framework.get_properties +dump_stacks := data.framework.dump_stacks +runtime_logging := data.framework.runtime_logging +load_fragment := data.framework.load_fragment +scratch_mount := data.framework.scratch_mount +scratch_unmount := data.framework.scratch_unmount +reason := { + "errors": data.framework.errors, + "error_objects": data.framework.error_objects, +} diff --git a/tests/opa.passing b/tests/opa.passing index 2f2810f..1e45e18 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -16,6 +16,7 @@ embeddedvirtualdoc evaltermexpr example fix1863 +indexing intersection invalidkeyerror jsonfilteridempotent @@ -26,6 +27,9 @@ objectremoveidempotent objectremovenonstringkey partialsetdoc rand +regexisvalid +regexmatch +regexsplit replacen semvercompare sets @@ -35,8 +39,10 @@ trim trimleft trimprefix trimright +trimspace trimsuffix typebuiltin typenamebuiltin +undos union units \ No newline at end of file diff --git a/tests/opa.rs b/tests/opa.rs index 8ea2a6d..3b38393 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -121,7 +121,12 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> { } (r, _) => { print!("\n{} failed.", case.note); - dbg!((&case, &r)); + println!("{}", serde_yaml::to_string(&case)?); + match &r { + Ok(actual) => println!("GOT\n{}", serde_yaml::to_string(&actual)?), + Err(e) => println!("ERROR: {e}"), + } + if let Err(e) = r { let msg = e.to_string(); let pat = "could not find function ";