//! The generator and the oracle of the policy property test. Do not edit. //! //! Included with `#[path = "support/oracle.rs"] mod oracle;`. //! //! The oracle is written to be obviously right, not fast or short. It shares no code with //! `brokerd`: it splits paths and host names into their parts itself and compares the parts. //! If the property test fails, the oracle is the specification and `policy` is wrong. #![allow(dead_code)] // the property test does not use every helper in every build use brokerd::grants::LoadedGrant; use brokerd::policy::SessionState; use proto::{ CallId, Constraints, DataClass, DenyReason, Grant, Mode, SessionId, Timestamp, ToolRequest, }; /// The moment every case is decided at: 2026-09-18T12:00:00.000Z. pub const NOW_MS: u64 = 1_789_732_800_000; // --------------------------------------------------------------------------------------------- // The generator. pub struct Rng(u64); impl Rng { pub fn new(seed: u64) -> Rng { // xorshift must not start at zero. Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1) } pub fn next(&mut self) -> u64 { let mut x = self.0; x ^= x << 13; x ^= x >> 7; x ^= x << 17; self.0 = x; x } pub fn below(&mut self, n: usize) -> usize { (self.next() % n as u64) as usize } pub fn pick(&mut self, items: &[T]) -> T { items[self.below(items.len())] } pub fn chance(&mut self, percent: u64) -> bool { self.next() % 100 < percent } } pub const TOOLS: [&str; 4] = ["read_file", "write_file", "shell", "http_fetch"]; pub const CLASSES: [DataClass; 3] = [DataClass::Public, DataClass::Private, DataClass::Secret]; pub const MODES: [Mode; 3] = [Mode::Auto, Mode::Ask, Mode::Deny]; pub const HOSTS: [&str; 6] = [ "example.com", "www.example.com", "a.b.example.com", "other.org", "www.other.org", "badexample.com", ]; pub const PATTERNS: [&str; 6] = [ "example.com", "*.example.com", "www.example.com", "*.b.example.com", "other.org", "*.other.org", ]; /// A path of one to four components over a tiny alphabet, so that grants and requests overlap /// often: `/a`, `/a/b`, `/ab/a/c` and so on. `ab` is there to catch prefix matching by bytes. pub fn path(rng: &mut Rng) -> String { let depth = 1 + rng.below(4); let mut text = String::new(); for _ in 0..depth { text.push('/'); text.push_str(rng.pick(&["a", "b", "c", "ab"])); } text } pub fn some_grant(rng: &mut Rng, id: usize) -> LoadedGrant { let tool = rng.pick(&TOOLS); let mode = rng.pick(&MODES); let mut constraints = Constraints::default(); match tool { "http_fetch" => { for _ in 0..1 + rng.below(2) { constraints.hosts.push(rng.pick(&PATTERNS).to_string()); } } "shell" if rng.chance(40) => {} _ => { for _ in 0..1 + rng.below(3) { constraints.paths.push(path(rng)); } } } // Expiry around the moment of decision: before it, exactly at it, after it, or never. let expires = match rng.below(5) { 0 => Some(NOW_MS - 1), 1 => Some(NOW_MS), 2 => Some(NOW_MS + 1), _ => None, }; LoadedGrant { id: format!("g{id:02}"), grant: Grant { tool: tool.to_string(), mode, // A deny grant must apply at every taint, or the set is invalid. max_taint: if mode == Mode::Deny { DataClass::Secret } else { rng.pick(&CLASSES) }, result_class: rng.pick(&CLASSES), untrusted: rng.chance(50), expires: expires.map(|ms| Timestamp::from_unix_millis(ms).unwrap()), secret: None, constraints, }, sha256: proto::sha256(format!("file {id}").as_bytes()).unwrap(), } } /// Zero to seven grants, with ids handed out in a scrambled order so that the order of the list /// says nothing about the order of the ids. pub fn some_grants(rng: &mut Rng) -> Vec { let count = rng.below(8); let mut ids: Vec = (0..count).collect(); for i in (1..ids.len()).rev() { ids.swap(i, rng.below(i + 1)); } ids.into_iter().map(|id| some_grant(rng, id)).collect() } pub fn some_request(rng: &mut Rng) -> ToolRequest { let (tool, arguments) = match rng.below(20) { 0 => ("echo".to_string(), "{}".to_string()), 1 => ("read_file".to_string(), r#"{"path":"a/b"}"#.to_string()), 2 => ( "shell".to_string(), r#"{"command":"ls","cwd":"/a/../b"}"#.to_string(), ), 3 => ( "http_fetch".to_string(), r#"{"url":"http://example.com/"}"#.to_string(), ), 4 => ("write_file".to_string(), r#"{"path":"/a/b"}"#.to_string()), _ => match rng.pick(&TOOLS) { "read_file" => ( "read_file".to_string(), format!(r#"{{"path":"{}"}}"#, path(rng)), ), "write_file" => ( "write_file".to_string(), format!(r#"{{"path":"{}","content":"x"}}"#, path(rng)), ), "shell" if rng.chance(40) => ("shell".to_string(), r#"{"command":"ls"}"#.to_string()), "shell" => ( "shell".to_string(), format!(r#"{{"command":"ls","cwd":"{}"}}"#, path(rng)), ), _ => ( "http_fetch".to_string(), format!(r#"{{"url":"https://{}/x"}}"#, rng.pick(&HOSTS)), ), }, }; ToolRequest { session: SessionId::new("prop").unwrap(), call: CallId(1), tool, arguments, } } // --------------------------------------------------------------------------------------------- // The oracle. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Expected { Allowed { grant: String, path: Option, class: DataClass, untrusted: bool, }, Ask { grant: String, path: Option, class: DataClass, untrusted: bool, }, Denied { reason: DenyReason, grant: Option, }, } pub fn parts(path: &str) -> Vec<&str> { path.split('/').filter(|part| !part.is_empty()).collect() } /// `inner` is `outer` or lies under it: `outer`'s components are the first of `inner`'s. pub fn under(outer: &str, inner: &str) -> bool { let (outer, inner) = (parts(outer), parts(inner)); outer.len() <= inner.len() && outer.iter().zip(&inner).all(|(a, b)| a == b) } pub fn host_fits(pattern: &str, host: &str) -> bool { let host: Vec<&str> = host.split('.').collect(); match pattern.strip_prefix("*.") { None => pattern.split('.').collect::>() == host, Some(base) => { let base: Vec<&str> = base.split('.').collect(); host.len() > base.len() && host[host.len() - base.len()..] == base[..] } } } /// What the oracle needs from a request: `None` if the broker must refuse it before matching. pub enum Call { UnknownTool, Invalid, Read(String), Write(String), Shell(Option), Fetch(String), } pub fn understand(request: &ToolRequest) -> Call { if !TOOLS.contains(&request.tool.as_str()) { return Call::UnknownTool; } // The generator only ever writes the five invalid forms below. let text = request.arguments.as_str(); let invalid = text.contains("\"a/b\"") || text.contains("..") || text.contains("http://") || (request.tool == "write_file" && !text.contains("content")); if invalid { return Call::Invalid; } let value: serde_json::Value = serde_json::from_str(text).unwrap(); let field = |name: &str| value.get(name).and_then(|v| v.as_str()).map(str::to_string); match request.tool.as_str() { "read_file" => Call::Read(field("path").unwrap()), "write_file" => Call::Write(field("path").unwrap()), "shell" => Call::Shell(field("cwd")), _ => { let url = field("url").unwrap(); let host = url .trim_start_matches("https://") .split('/') .next() .unwrap(); Call::Fetch(host.to_string()) } } } /// Whether the grant covers the call, and with which of its paths (the longest that holds it). pub fn coverage(grant: &Grant, call: &Call) -> Option> { let holding = |path: &str, itself_counts: bool| -> Option> { let mut best: Option<&String> = None; for candidate in &grant.constraints.paths { if !under(candidate, path) || (!itself_counts && parts(candidate) == parts(path)) { continue; } if best.is_none_or(|b| candidate.len() > b.len()) { best = Some(candidate); } } best.map(|b| Some(b.clone())) }; match call { Call::Read(path) => holding(path, true), Call::Write(path) => holding(path, false), Call::Shell(None) if grant.constraints.paths.is_empty() => Some(None), Call::Shell(None) => None, Call::Shell(Some(cwd)) => holding(cwd, true), Call::Fetch(host) => grant .constraints .hosts .iter() .any(|pattern| host_fits(pattern, host)) .then_some(None), Call::UnknownTool | Call::Invalid => None, } } pub fn oracle(request: &ToolRequest, grants: &[LoadedGrant], state: SessionState) -> Expected { let denied = |reason| Expected::Denied { reason, grant: None, }; let call = understand(request); match call { Call::UnknownTool => return denied(DenyReason::NoGrant), Call::Invalid => return denied(DenyReason::InvalidArguments), _ => {} } struct Left<'a> { id: &'a str, mode: Mode, path: Option, class: DataClass, untrusted: bool, } let mut left: Vec = Vec::new(); let (mut would_match_but_expired, mut would_match_but_tainted) = (false, false); for loaded in grants { let g = &loaded.grant; if g.tool != request.tool { continue; } let Some(path) = coverage(g, &call) else { continue; }; let expired = g.expires.is_some_and(|at| at.unix_millis() <= NOW_MS); let tainted = state.taint > g.max_taint; if expired && !tainted { would_match_but_expired = true; } if tainted && !expired { would_match_but_tainted = true; } if !expired && !tainted { left.push(Left { id: &loaded.id, mode: g.mode, path, class: g.result_class, untrusted: g.untrusted, }); } } if left.is_empty() { return if would_match_but_expired { denied(DenyReason::GrantExpired) } else if would_match_but_tainted { denied(DenyReason::TaintTooHigh) } else { denied(DenyReason::NoGrant) }; } let class = left.iter().map(|l| l.class).max().unwrap(); let untrusted = left.iter().any(|l| l.untrusted); // The winner: try each mode from the most restrictive; within it the longest path, then // the lowest id. for mode in [Mode::Deny, Mode::Ask, Mode::Auto] { let mut of_mode: Vec<&Left> = left.iter().filter(|l| l.mode == mode).collect(); if of_mode.is_empty() { continue; } of_mode.sort_by(|a, b| { let (la, lb) = ( a.path.as_ref().map_or(0, String::len), b.path.as_ref().map_or(0, String::len), ); lb.cmp(&la).then(a.id.cmp(b.id)) }); let winner = of_mode[0]; let (grant, path) = (winner.id.to_string(), winner.path.clone()); return match mode { Mode::Deny => Expected::Denied { reason: DenyReason::DeniedByGrant, grant: Some(grant), }, Mode::Ask => Expected::Ask { grant, path, class, untrusted, }, Mode::Auto => Expected::Allowed { grant, path, class, untrusted, }, }; } unreachable!("left is not empty, so one of the three modes has a grant") }