From ec6a6edab7f9ccb5dbe2748edc7b510a7b7a4ffc Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Oct 2026 12:21:36 -0500 Subject: [PATCH] Rename escalation mechanism labels The mechanism shown for an escalation hop was "pods/exec" or "pods create". The first is also the literal Kubernetes subresource name, so a hop label could be mistaken for an RBAC resource. The second contains a space, which reads poorly in the comma-joined list (for example "pods/exec, token, pods create, impersonate") and is awkward to split or grep. Use "pod-exec" and "pod-create" instead. These appear in the text output of ::smt reaches and ::smt cluster-admin, and as the "mechanism" field in --format ndjson output, so consumers matching the old strings need to be updated. The Mangle rules and examples that refer to the real pods/exec resource are unchanged. Add a test that drives the real pipeline and asserts both labels. Co-Authored-By: Claude Sonnet 5.5 --- src/smt/access.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/smt/access.rs b/src/smt/access.rs index fb76236..fee2385 100644 --- a/src/smt/access.rs +++ b/src/smt/access.rs @@ -748,7 +748,7 @@ impl<'ctx> SmtEncoder<'ctx> { } /// Return the direct escalation mechanism(s) from `principal` to `target` in one hop. - /// Returns a comma-separated string, e.g. "pods/exec, token". Empty string if unknown. + /// Returns a comma-separated string, e.g. "pod-exec, token". Empty string if unknown. fn mechanism_for(&self, principal: &str, target: &str) -> Option { use mangle_common::Value; @@ -780,7 +780,7 @@ impl<'ctx> SmtEncoder<'ctx> { .get("exec_reachable_sa") .is_some_and(|r| r.iter().any(|row| matches_sa(row, 1, 2))) { - mechanisms.push("pods/exec"); + mechanisms.push("pod-exec"); } if self .facts @@ -794,7 +794,7 @@ impl<'ctx> SmtEncoder<'ctx> { .get("pod_creatable_sa") .is_some_and(|r| r.iter().any(|row| matches_sa(row, 1, 2))) { - mechanisms.push("pods create"); + mechanisms.push("pod-create"); } if self .facts @@ -1094,4 +1094,47 @@ mod tests { "builtin missing with include_builtins: {shown:?}" ); } + + #[test] + fn pod_exec_and_pod_create_hops_use_hyphenated_mechanism_labels() { + // demo:target holds cluster-admin and runs a pod. exec-user can only + // exec into pods in demo; create-user can only create pods in demo. + let role = |name: &str, resource: &str| { + format!( + r#"{{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"Role","metadata":{{"name":"{name}","namespace":"demo"}},"rules":[{{"apiGroups":[""],"resources":["{resource}"],"verbs":["create"]}}]}}"# + ) + }; + let binding = |name: &str, user: &str| { + format!( + r#"{{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{{"name":"{name}","namespace":"demo"}},"roleRef":{{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"{name}"}},"subjects":[{{"kind":"User","name":"{user}"}}]}}"# + ) + }; + let extra = [ + r#"{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"name":"target","namespace":"demo"}}"#.to_string(), + r#"{"apiVersion":"v1","kind":"Pod","metadata":{"name":"p","namespace":"demo"},"spec":{"serviceAccountName":"target","containers":[{"name":"c","image":"busybox"}]}}"#.to_string(), + r#"{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRoleBinding","metadata":{"name":"target-admin"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"cluster-admin"},"subjects":[{"kind":"ServiceAccount","name":"target","namespace":"demo"}]}"#.to_string(), + role("exec-role", "pods/exec"), + binding("exec-role", "exec-user"), + role("create-role", "pods"), + binding("create-role", "create-user"), + ] + .join("\n"); + let eval = load_engine_with(&extra).evaluate().expect("evaluate"); + + let cfg = z3::Config::new(); + let ctx = z3::Context::new(&cfg); + let mut enc = SmtEncoder::new(&ctx); + enc.assert_rbac_axioms(&eval); + + let mechanisms = |user: &str| -> Vec { + enc.paths_for_principal(user, "", Some(("", "*", "*", "*"))) + .into_iter() + .flat_map(|p| p.hops) + .filter(|(id, _)| id == "system:serviceaccount:demo:target") + .map(|(_, mech)| mech) + .collect() + }; + assert_eq!(mechanisms("exec-user"), vec!["pod-exec"]); + assert_eq!(mechanisms("create-user"), vec!["pod-create"]); + } }