use std::collections::HashMap;
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
use regex_automata::{meta, nfa::thompson::WhichCaptures, util::syntax, Input, PatternSet};
use serde_json::Value;
use crate::generated::types::{T1Leaf, T1Node, T1PredicateTree, Verdict};
use super::bundle::{ArtifactBody, LoadedArtifact};
use super::facts;
use super::kleene::Kleene;
use super::tier2::{build, declared_mode, on_inconclusive_verdict};
use super::types::{Contribution, EvalContext};
pub const KNOWN_PREDS: &[&str] = &[
"equals",
"in_set",
"prefix",
"glob",
"keyword",
"regex_lite",
"tld_in",
"int_cmp",
"exists",
"effect",
"fact",
];
pub const KNOWN_NODE_OPS: &[&str] = &["and", "or", "not", "leaf"];
pub const KNOWN_FACT_OPS: &[&str] = &["equals", "in_set", "int_cmp"];
pub const KNOWN_FIELDS: &[&str] = &[
"tool.name",
"input.strings",
"result.strings",
"result.exit_code",
"command.program",
"command.argv",
"command.simple",
"path.class",
"path.value",
"url.host",
"url.tld",
"url.scheme",
"url.boundary",
"effect.verb",
"effect.target_class",
"agent.type",
"agent.environment",
"agent.function",
"agent.id",
"agent.principal",
"session.elapsed_ms",
"session.tool_calls",
"session.spend_micro_usd",
"session.tokens",
];
pub const FIELD_PREFIXES: &[&str] = &["input.", "fact.", "effect.attrs."];
pub const SHELL_TOOLS: &[&str] = &["Bash", "BashOutput", "PowerShell"];
pub const REGEX_SOURCE_LIMIT: usize = 1024;
pub const REGEX_SIZE_LIMIT: usize = 10 * (1 << 20);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PatternKind {
Keyword,
Prefix,
Regex,
Glob,
}
#[derive(Debug, Clone, Default)]
pub struct ScanTable {
pub keyword_sources: Vec<String>,
pub prefix_sources: Vec<String>,
pub regex_sources: Vec<String>,
pub glob_sources: Vec<String>,
pub scanned_fields: Vec<String>,
keyword_index: HashMap<String, usize>,
prefix_index: HashMap<String, usize>,
regex_index: HashMap<String, usize>,
glob_index: HashMap<String, usize>,
keyword_ac: Option<AhoCorasick>,
prefix_ac: Option<AhoCorasick>,
regex_set: Option<meta::Regex>,
glob_set: Option<meta::Regex>,
}
impl PartialEq for ScanTable {
fn eq(&self, other: &Self) -> bool {
self.keyword_sources == other.keyword_sources
&& self.prefix_sources == other.prefix_sources
&& self.regex_sources == other.regex_sources
&& self.glob_sources == other.glob_sources
}
}
impl ScanTable {
pub fn compile(artifacts: &[LoadedArtifact]) -> ScanTable {
let mut table = ScanTable::default();
for artifact in artifacts {
let node = match &artifact.body {
ArtifactBody::T1(body) => body.node.as_ref(),
ArtifactBody::T3(body) => body.trigger.as_ref(),
_ => None,
};
if let Some(node) = node {
walk_leaves(node, &mut |leaf| table.collect(leaf));
}
}
table.finish();
table
}
fn collect(&mut self, leaf: &T1Leaf) {
let Some(pred) = leaf.pred.as_deref() else {
return;
};
let kind = match pred {
"keyword" => PatternKind::Keyword,
"prefix" => PatternKind::Prefix,
"regex_lite" => PatternKind::Regex,
"glob" => PatternKind::Glob,
_ => return,
};
let Some(field) = leaf.field.as_deref() else {
return;
};
if !self.scanned_fields.iter().any(|f| f == field) {
self.scanned_fields.push(field.to_string());
}
for source in pattern_sources(leaf, kind) {
let (sources, index) = match kind {
PatternKind::Keyword => (&mut self.keyword_sources, &mut self.keyword_index),
PatternKind::Prefix => (&mut self.prefix_sources, &mut self.prefix_index),
PatternKind::Regex => (&mut self.regex_sources, &mut self.regex_index),
PatternKind::Glob => (&mut self.glob_sources, &mut self.glob_index),
};
let key = match kind {
PatternKind::Keyword => fold_keyword(&source).into_owned(),
_ => source.clone(),
};
if let std::collections::hash_map::Entry::Vacant(slot) = index.entry(key) {
let id = sources.len();
slot.insert(id);
sources.push(source);
}
}
}
fn finish(&mut self) {
if !self.keyword_sources.is_empty() {
let folded: Vec<String> = self
.keyword_sources
.iter()
.map(|source| fold_keyword(source).into_owned())
.collect();
self.keyword_ac = AhoCorasickBuilder::new()
.match_kind(MatchKind::Standard)
.build(&folded)
.ok();
}
if !self.prefix_sources.is_empty() {
self.prefix_ac = AhoCorasickBuilder::new()
.match_kind(MatchKind::Standard)
.build(&self.prefix_sources)
.ok();
}
if !self.regex_sources.is_empty() {
self.regex_set = compile_regex_set(&self.regex_sources);
}
if !self.glob_sources.is_empty() {
let translated: Vec<String> =
self.glob_sources.iter().map(|g| glob_to_regex(g)).collect();
self.glob_set = compile_pattern_set(&translated, true);
}
}
pub fn scan(&self, ctx: &EvalContext<'_>) -> ScanResult {
let mut result = ScanResult::default();
if self.scanned_fields.is_empty() {
return result;
}
for field in &self.scanned_fields {
let mut matches = FieldMatches {
keyword: vec![false; self.keyword_sources.len()],
prefix: vec![false; self.prefix_sources.len()],
regex: vec![false; self.regex_sources.len()],
glob: vec![false; self.glob_sources.len()],
};
for value in field_values(field, ctx) {
let Some(text) = value.as_str() else {
continue;
};
if let Some(automaton) = &self.keyword_ac {
let folded = fold_keyword(text);
for hit in automaton.find_overlapping_iter(folded.as_ref()) {
matches.keyword[hit.pattern().as_usize()] = true;
}
}
if let Some(automaton) = &self.prefix_ac {
for hit in automaton.find_overlapping_iter(text) {
if hit.start() == 0 {
matches.prefix[hit.pattern().as_usize()] = true;
}
}
}
if let Some(set) = &self.regex_set {
for id in matching_patterns(set, text.as_bytes()).iter() {
matches.regex[id.as_usize()] = true;
}
}
if let Some(set) = &self.glob_set {
for id in matching_patterns(set, text.as_bytes()).iter() {
matches.glob[id.as_usize()] = true;
}
}
}
result.fields.push((field.clone(), matches));
}
result
}
fn lookup(
&self,
result: &ScanResult,
field: &str,
kind: PatternKind,
source: &str,
) -> Option<bool> {
let index = match kind {
PatternKind::Keyword => &self.keyword_index,
PatternKind::Prefix => &self.prefix_index,
PatternKind::Regex => &self.regex_index,
PatternKind::Glob => &self.glob_index,
};
let id = match kind {
PatternKind::Keyword => *index.get(fold_keyword(source).as_ref())?,
_ => *index.get(source)?,
};
let matches = &result.fields.iter().find(|(name, _)| name == field)?.1;
let bits = match kind {
PatternKind::Keyword => &matches.keyword,
PatternKind::Prefix => &matches.prefix,
PatternKind::Regex => &matches.regex,
PatternKind::Glob => &matches.glob,
};
bits.get(id).copied()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScanResult {
fields: Vec<(String, FieldMatches)>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct FieldMatches {
keyword: Vec<bool>,
prefix: Vec<bool>,
regex: Vec<bool>,
glob: Vec<bool>,
}
fn fold_keyword(text: &str) -> std::borrow::Cow<'_, str> {
if !text
.bytes()
.any(|b| b.is_ascii_uppercase() || !b.is_ascii())
{
return std::borrow::Cow::Borrowed(text);
}
if text.is_ascii() {
std::borrow::Cow::Owned(text.to_ascii_lowercase())
} else {
std::borrow::Cow::Owned(caseless::default_case_fold_str(text))
}
}
fn pattern_builder(utf8: bool) -> meta::Builder {
let mut builder = meta::Regex::builder();
builder
.configure(
meta::Regex::config()
.match_kind(regex_automata::MatchKind::LeftmostFirst)
.utf8_empty(utf8)
.nfa_size_limit(Some(REGEX_SIZE_LIMIT))
.hybrid_cache_capacity(2 * (1 << 20))
.pool_capacity(8),
)
.syntax(syntax::Config::new().utf8(utf8).unicode(utf8));
builder
}
fn compile_pattern_set(sources: &[String], utf8: bool) -> Option<meta::Regex> {
pattern_builder(utf8)
.configure(
meta::Regex::config()
.match_kind(regex_automata::MatchKind::All)
.which_captures(WhichCaptures::None)
.nfa_size_limit(Some(REGEX_SIZE_LIMIT.saturating_mul(sources.len().max(1)))),
)
.build_many(sources)
.ok()
}
fn matching_patterns(set: &meta::Regex, text: &[u8]) -> PatternSet {
let mut matches = PatternSet::new(set.pattern_len());
set.which_overlapping_matches(&Input::new(text), &mut matches);
matches
}
fn compile_regex_set(sources: &[String]) -> Option<meta::Regex> {
compile_pattern_set(sources, false)
}
fn compile_one_regex(source: &str) -> Option<meta::Regex> {
pattern_builder(false).build(source).ok()
}
fn compile_one_glob(source: &str) -> Option<meta::Regex> {
pattern_builder(true).build(&glob_to_regex(source)).ok()
}
fn glob_to_regex(pattern: &str) -> String {
let mut out = String::from("(?s)\\A");
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '*' && i + 1 < chars.len() && chars[i + 1] == '*' {
out.push_str(".*");
i += 2;
continue;
}
match chars[i] {
'*' => out.push_str("[^/]*"),
'?' => out.push_str("[^/]"),
other => out.push_str(®ex::escape(&other.to_string())),
}
i += 1;
}
out.push_str("\\z");
out
}
fn walk_leaves(node: &T1Node, visit: &mut impl FnMut(&T1Leaf)) {
if let Some(leaf) = node.leaf.as_ref() {
visit(leaf);
}
for child in &node.children {
walk_leaves(child, visit);
}
}
fn pattern_sources(leaf: &T1Leaf, kind: PatternKind) -> Vec<String> {
let carrier = match kind {
PatternKind::Keyword | PatternKind::Prefix => leaf.value.as_ref(),
PatternKind::Regex | PatternKind::Glob => leaf
.pattern
.as_ref()
.map(|_| None)
.unwrap_or_else(|| leaf.value.as_ref())
.or(None),
};
let mut out = Vec::new();
if matches!(kind, PatternKind::Regex | PatternKind::Glob) {
if let Some(pattern) = leaf.pattern.as_ref() {
out.push(pattern.clone());
return out;
}
}
if let Some(value) = carrier.or(leaf.value.as_ref()) {
for item in as_list(value) {
if let Some(text) = item.as_str() {
out.push(text.to_string());
}
}
}
out
}
pub fn validate_node(node: Option<&T1Node>) -> Result<(), String> {
let Some(node) = node else {
return Err("t1 body carries no node".to_string());
};
let op = node.op.as_deref().unwrap_or("");
if !KNOWN_NODE_OPS.contains(&op) {
return Err(format!("unknown node op '{op}'"));
}
if op == "leaf" {
let Some(leaf) = node.leaf.as_ref() else {
return Err("leaf node without a leaf".to_string());
};
return validate_leaf(leaf);
}
if node.children.is_empty() {
return Err(format!("{op} node without children"));
}
if op == "not" && node.children.len() != 1 {
return Err(format!(
"not takes exactly one child, got {}",
node.children.len()
));
}
for child in &node.children {
validate_node(Some(child))?;
}
Ok(())
}
fn validate_leaf(leaf: &T1Leaf) -> Result<(), String> {
let pred = leaf.pred.as_deref().unwrap_or("");
if !KNOWN_PREDS.contains(&pred) {
return Err(format!("unknown predicate '{pred}'"));
}
if pred == "fact" {
let op = leaf
.fact
.as_ref()
.and_then(|f| f.op.as_deref())
.unwrap_or("");
if !KNOWN_FACT_OPS.contains(&op) {
return Err(format!("unknown fact op '{op}'"));
}
return Ok(());
}
if pred == "effect" {
return Ok(());
}
let Some(field) = leaf.field.as_deref() else {
return Err("leaf without a field".to_string());
};
if !KNOWN_FIELDS.contains(&field) && !FIELD_PREFIXES.iter().any(|p| field.starts_with(p)) {
return Err(format!("unknown field '{field}'"));
}
Ok(())
}
pub fn validate_patterns(node: Option<&T1Node>) -> Result<(), String> {
let Some(node) = node else {
return Ok(());
};
let mut failure: Option<String> = None;
walk_leaves(node, &mut |leaf| {
if failure.is_some() {
return;
}
failure = validate_leaf_patterns(leaf).err();
});
match failure {
Some(reason) => Err(reason),
None => Ok(()),
}
}
fn validate_leaf_patterns(leaf: &T1Leaf) -> Result<(), String> {
match leaf.pred.as_deref() {
Some("regex_lite") => {
let sources = pattern_sources(leaf, PatternKind::Regex);
if sources.is_empty() {
return Err("regex_lite without a pattern".to_string());
}
for source in sources {
if source.len() > REGEX_SOURCE_LIMIT {
return Err(format!(
"regex_lite over the {REGEX_SOURCE_LIMIT}-byte source limit"
));
}
if compile_one_regex(&source).is_none() {
return Err(format!("regex_lite does not compile: {source}"));
}
}
Ok(())
}
Some("glob") => {
let sources = pattern_sources(leaf, PatternKind::Glob);
if sources.is_empty() {
return Err("glob without a pattern".to_string());
}
for source in sources {
if compile_one_glob(&source).is_none() {
return Err(format!("glob does not compile: {source}"));
}
}
Ok(())
}
_ => Ok(()),
}
}
fn all_strings(value: &Value, out: &mut Vec<Value>) {
match value {
Value::String(_) => out.push(value.clone()),
Value::Object(map) => {
for child in map.values() {
all_strings(child, out);
}
}
Value::Array(items) => {
for child in items {
all_strings(child, out);
}
}
_ => {}
}
}
fn json_pointer<'a>(document: &'a Value, pointer: &str) -> Option<&'a Value> {
if pointer.is_empty() || pointer == "/" {
return Some(document);
}
if !pointer.starts_with('/') {
return None;
}
let mut current = document;
for raw in pointer[1..].split('/') {
let token = raw.replace("~1", "/").replace("~0", "~");
match current {
Value::Object(map) => current = map.get(&token)?,
Value::Array(items) => {
if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let index: usize = token.parse().ok()?;
current = items.get(index)?;
}
_ => return None,
}
}
Some(current)
}
fn exit_code(event: &super::types::Event) -> Option<i64> {
if event.event_type != "post_tool_use" {
return None;
}
if !SHELL_TOOLS.contains(&event.tool_name.as_str()) {
return None;
}
let result = event.tool_result.as_ref()?.as_object()?;
for key in ["exit_code", "exitCode", "returncode"] {
if let Some(code) = result.get(key).and_then(Value::as_i64) {
return Some(code);
}
}
None
}
pub fn field_values(field: &str, ctx: &EvalContext<'_>) -> Vec<Value> {
let event = ctx.event;
let class = ctx.classification;
match field {
"tool.name" => {
if event.tool_name.is_empty() {
Vec::new()
} else {
vec![Value::String(event.tool_name.clone())]
}
}
"input.strings" => {
let mut out = Vec::new();
all_strings(&event.tool_input, &mut out);
out
}
"result.strings" => {
let mut out = Vec::new();
if let Some(result) = event.tool_result.as_ref() {
all_strings(result, &mut out);
}
out
}
"result.exit_code" => exit_code(event).map(Value::from).into_iter().collect(),
"command.program" => class
.simple
.iter()
.filter(|s| !s.program.is_empty())
.map(|s| Value::String(s.program.clone()))
.collect(),
"command.argv" => class
.simple
.iter()
.flat_map(|s| s.argv.iter())
.map(|a| Value::String(a.clone()))
.collect(),
"command.simple" => class.simple.iter().map(|s| s.as_value()).collect(),
"path.class" => class
.paths
.iter()
.map(|p| Value::String(p.class.0.clone()))
.collect(),
"path.value" => class
.paths
.iter()
.map(|p| Value::String(p.value.clone()))
.collect(),
"url.host" => class
.urls
.iter()
.map(|u| Value::String(u.host.clone()))
.collect(),
"url.tld" => class
.urls
.iter()
.map(|u| Value::String(u.tld.clone()))
.collect(),
"url.scheme" => class
.urls
.iter()
.map(|u| Value::String(u.scheme.clone()))
.collect(),
"url.boundary" => class
.urls
.iter()
.map(|u| Value::String(u.boundary.as_str().to_string()))
.collect(),
"effect.verb" => class
.effects
.iter()
.map(|e| Value::String(e.verb.0.clone()))
.collect(),
"effect.target_class" => class
.effects
.iter()
.map(|e| Value::String(e.target_class.0.clone()))
.collect(),
_ => {
if let Some(pointer) = field.strip_prefix("input.") {
return json_pointer(&event.tool_input, pointer)
.cloned()
.into_iter()
.collect();
}
if let Some(key) = field.strip_prefix("agent.") {
let Some(agent) = event.agent.as_ref() else {
return Vec::new();
};
let value = match key {
"type" => agent.agent_type.as_ref(),
"id" => agent.agent_id.as_ref(),
"environment" => agent.environment.as_ref(),
"function" => agent.function.as_ref(),
"principal" => agent.principal.as_ref(),
_ => None,
};
return value
.map(|v| Value::String(v.clone()))
.into_iter()
.collect();
}
if let Some(key) = field.strip_prefix("session.") {
let Some(session) = event.session.as_ref() else {
return Vec::new();
};
let value = match key {
"elapsed_ms" => session.elapsed_ms,
"tool_calls" => session.tool_calls,
"spend_micro_usd" => session.spend_micro_usd,
"tokens" => session.tokens,
_ => None,
};
return value.map(Value::from).into_iter().collect();
}
Vec::new()
}
}
}
fn as_list(value: &Value) -> Vec<&Value> {
match value {
Value::Array(items) => items.iter().collect(),
other => vec![other],
}
}
fn int_cmp(left: &Value, spec: Option<&Value>) -> bool {
let (Some(spec), Some(left)) = (spec.and_then(Value::as_object), left.as_i64()) else {
return false;
};
let Some(n) = spec.get("n").and_then(Value::as_i64) else {
return false;
};
match spec.get("op").and_then(Value::as_str) {
Some("lt") => left < n,
Some("le") => left <= n,
Some("gt") => left > n,
Some("ge") => left >= n,
Some("eq") => left == n,
_ => false,
}
}
fn test_predicate(
pred: &str,
field: &str,
value: &Value,
leaf: &T1Leaf,
scan: &ScanTable,
result: &ScanResult,
) -> bool {
match pred {
"equals" => leaf.value.as_ref().is_some_and(|literal| value == literal),
"in_set" | "tld_in" => leaf
.value
.as_ref()
.is_some_and(|literal| as_list(literal).into_iter().any(|item| item == value)),
"prefix" => {
let Some(text) = value.as_str() else {
return false;
};
pattern_sources(leaf, PatternKind::Prefix)
.iter()
.any(|source| {
scan.lookup(result, field, PatternKind::Prefix, source)
.unwrap_or_else(|| text.starts_with(source.as_str()))
})
}
"keyword" => {
let Some(text) = value.as_str() else {
return false;
};
pattern_sources(leaf, PatternKind::Keyword)
.iter()
.any(|source| {
scan.lookup(result, field, PatternKind::Keyword, source)
.unwrap_or_else(|| {
fold_keyword(text).contains(fold_keyword(source).as_ref())
})
})
}
"glob" => {
let Some(text) = value.as_str() else {
return false;
};
pattern_sources(leaf, PatternKind::Glob)
.iter()
.any(|source| {
scan.lookup(result, field, PatternKind::Glob, source)
.unwrap_or_else(|| {
compile_one_glob(source).is_some_and(|re| re.is_match(text))
})
})
}
"regex_lite" => {
let Some(text) = value.as_str() else {
return false;
};
pattern_sources(leaf, PatternKind::Regex)
.iter()
.any(|source| {
scan.lookup(result, field, PatternKind::Regex, source)
.unwrap_or_else(|| {
compile_one_regex(source).is_some_and(|re| re.is_match(text.as_bytes()))
})
})
}
"int_cmp" => int_cmp(value, leaf.value.as_ref()),
_ => false,
}
}
fn note_fact_id(ctx: &mut EvalContext<'_>, fact_id: &str) {
if !facts::is_known_fact_id(fact_id) {
ctx.warn(format!(
"fact_id '{fact_id}' is not a known FactId \
(schemas/enums.schema.json $defs/FactId x-known-values)"
));
}
}
fn note_if_fact_unresolved(ctx: &mut EvalContext<'_>, fact_id: &str, value: Kleene) {
if value.reason().is_some_and(|reason| reason.names_a_fact()) {
ctx.note_inconclusive(fact_id);
}
}
fn leaf_fact_subject(leaf: &T1Leaf, ctx: &mut EvalContext<'_>) -> Kleene {
let spec = leaf.fact.as_ref();
let fact_id = spec
.and_then(|f| f.fact_id.as_ref())
.map(|id| id.0.clone())
.unwrap_or_default();
let op = spec.and_then(|f| f.op.as_deref()).unwrap_or("");
let wanted = spec.and_then(|f| f.value.as_ref());
note_fact_id(ctx, &fact_id);
let value = facts::resolve_leaf(ctx.facts, &fact_id, op, wanted, ctx.now_ms);
note_if_fact_unresolved(ctx, &fact_id, value);
value
}
fn leaf_effect_tuple(leaf: &T1Leaf, ctx: &EvalContext<'_>) -> Kleene {
let Some(wanted) = leaf.effect.as_ref() else {
return Kleene::False;
};
let hit = ctx.classification.effects.iter().any(|tuple| {
wanted.verb.as_ref().is_some_and(|v| *v == tuple.verb)
&& wanted
.target_class
.as_ref()
.is_some_and(|c| *c == tuple.target_class)
});
Kleene::from_bool(hit)
}
fn leaf_effect_attribute(
leaf: &T1Leaf,
pred: &str,
field: &str,
ctx: &EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Kleene {
let name = &field["effect.attrs.".len()..];
let values: Vec<&Value> = ctx
.classification
.effects
.iter()
.filter_map(|tuple| tuple.attrs.get(name))
.collect();
if values.is_empty() {
return Kleene::UNKNOWN;
}
if pred == "exists" {
return Kleene::True;
}
Kleene::from_bool(
values
.into_iter()
.any(|value| test_predicate(pred, field, value, leaf, scan, result)),
)
}
fn fact_field_values<'a>(ctx: &mut EvalContext<'a>, field: &str) -> Option<Vec<&'a Value>> {
let fact_id = facts::split_field(field)
.map(|(id, _)| id.to_string())
.unwrap_or_default();
note_fact_id(ctx, &fact_id);
match facts::field_values(ctx.facts, field, ctx.now_ms) {
Ok(values) => Some(values),
Err(reason) => {
if reason.names_a_fact() {
ctx.note_inconclusive(&fact_id);
}
None
}
}
}
fn leaf_attribute(
leaf: &T1Leaf,
pred: &str,
field: &str,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Kleene {
let values = field_values(field, ctx);
if let Some(set_ref) = leaf.set_ref.as_ref() {
let fact_id = set_ref.0.clone();
note_fact_id(ctx, &fact_id);
if values.is_empty() {
return Kleene::False;
}
let each: Vec<Kleene> = values
.iter()
.map(|value| facts::membership(ctx.facts, &fact_id, value, ctx.now_ms))
.collect();
let folded = Kleene::any(&each);
note_if_fact_unresolved(ctx, &fact_id, folded);
return folded;
}
if pred == "exists" {
return Kleene::from_bool(!values.is_empty());
}
Kleene::from_bool(
values
.iter()
.any(|value| test_predicate(pred, field, value, leaf, scan, result)),
)
}
pub fn evaluate_leaf(
leaf: &T1Leaf,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Kleene {
let pred = leaf.pred.as_deref().unwrap_or("");
if pred == "fact" {
return leaf_fact_subject(leaf, ctx);
}
if pred == "effect" {
return leaf_effect_tuple(leaf, ctx);
}
let field = leaf.field.as_deref().unwrap_or("").to_string();
if field.starts_with("effect.attrs.") {
return leaf_effect_attribute(leaf, pred, &field, ctx, scan, result);
}
if field.starts_with("fact.") {
let Some(values) = fact_field_values(ctx, &field) else {
return Kleene::UNKNOWN;
};
if pred == "exists" {
return Kleene::from_bool(!values.is_empty());
}
return Kleene::from_bool(
values
.iter()
.any(|value| test_predicate(pred, &field, value, leaf, scan, result)),
);
}
leaf_attribute(leaf, pred, &field, ctx, scan, result)
}
pub fn evaluate_node(node: &T1Node, ctx: &mut EvalContext<'_>, scan: &ScanTable) -> Kleene {
let result = scan.scan(ctx);
evaluate_node_with(node, ctx, scan, &result)
}
pub fn evaluate_node_with(
node: &T1Node,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Kleene {
match node.op.as_deref().unwrap_or("") {
"leaf" => match node.leaf.as_ref() {
Some(leaf) => evaluate_leaf(leaf, ctx, scan, result),
None => Kleene::UNKNOWN,
},
"and" => {
let children = evaluate_children(node, ctx, scan, result);
Kleene::all(&children)
}
"or" => {
let children = evaluate_children(node, ctx, scan, result);
Kleene::any(&children)
}
"not" => {
let children = evaluate_children(node, ctx, scan, result);
match children.len() {
1 => !children[0],
_ => Kleene::UNKNOWN,
}
}
_ => Kleene::UNKNOWN,
}
}
fn evaluate_children(
node: &T1Node,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Vec<Kleene> {
node.children
.iter()
.map(|child| evaluate_node_with(child, ctx, scan, result))
.collect()
}
pub fn contribution(
artifact: &LoadedArtifact,
body: &T1PredicateTree,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
) -> Option<Contribution> {
let result = scan.scan(ctx);
contribution_with(artifact, body, ctx, scan, &result)
}
pub fn contribution_with(
artifact: &LoadedArtifact,
body: &T1PredicateTree,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
result: &ScanResult,
) -> Option<Contribution> {
let node = body.node.as_ref()?;
let mut child = ctx.fork();
let value = evaluate_node_with(node, &mut child, scan, result);
ctx.merge_warnings(&child);
if value == Kleene::False {
return None;
}
let inconclusive = !value.is_known();
let mode = declared_mode(artifact);
let verdict = if inconclusive {
on_inconclusive_verdict(artifact, &mode)
} else {
body.verdict.unwrap_or(Verdict::Block)
};
Some(build(
artifact,
mode,
verdict,
body.reason.clone().unwrap_or_default(),
if inconclusive {
child.inconclusive.clone()
} else {
Vec::new()
},
Vec::new(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::types::{EffectVerb, TargetClass};
use crate::zone_eval::types::{
AgentContext, Classification, ClassifiedPath, ClassifiedUrl, Effect, Event, SessionFacts,
SimpleCommand, UrlBoundary,
};
fn node(json: serde_json::Value) -> T1Node {
serde_json::from_value(json).expect("the fixture node parses")
}
fn artifact_of(node_json: serde_json::Value) -> LoadedArtifact {
let doc = serde_json::json!({
"schema_version": 2,
"organization_id": "org",
"revision": 1,
"built_at": "2026-09-01T00:00:00Z",
"enforcement_enabled": true,
"signature": null,
"artifacts": [{
"artifact_id": "probe",
"atom_id": "atom-probe",
"kind": "t1_predicate_tree",
"on_inconclusive": "ask",
"tier": 1,
"body": {"node": node_json, "verdict": "block", "reason": "probe fired"},
}],
});
super::super::bundle::load(doc)
.expect("the fixture bundle loads")
.artifacts
.remove(0)
}
fn run(
node_json: serde_json::Value,
event: &Event,
class: &Classification,
set: &super::super::facts::FactSet,
) -> (Kleene, Vec<String>) {
let tree = node(node_json);
let table = ScanTable::compile(&[artifact_of(
serde_json::to_value(&tree).expect("the tree serialises"),
)]);
let mut ctx = EvalContext::new(event, class, set, 1_756_742_400_000);
let value = evaluate_node(&tree, &mut ctx, &table);
(value, ctx.inconclusive.clone())
}
fn value_of(node_json: serde_json::Value, event: &Event, class: &Classification) -> Kleene {
let set = super::super::facts::FactSet::default();
run(node_json, event, class, &set).0
}
fn bash(command: &str) -> Event {
Event {
event_type: "pre_tool_use".to_string(),
tool_name: "Bash".to_string(),
tool_input: serde_json::json!({"command": command}),
..Event::default()
}
}
fn leaf(pred: &str, field: &str, value: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"op": "leaf",
"leaf": {"pred": pred, "field": field, "value": value},
})
}
fn effect(verb: &str, target: &str) -> Effect {
Effect {
verb: EffectVerb(verb.to_string()),
target_class: TargetClass(target.to_string()),
attrs: serde_json::Map::new(),
}
}
#[test]
fn equals_reads_tool_name() {
let event = bash("rm -rf /data/x");
let class = Classification::default();
assert_eq!(
value_of(leaf("equals", "tool.name", "Bash".into()), &event, &class),
Kleene::True
);
assert_eq!(
value_of(leaf("equals", "tool.name", "Read".into()), &event, &class),
Kleene::False
);
}
#[test]
fn in_set_matches_any_member() {
let event = Event {
tool_name: "Edit".to_string(),
..Event::default()
};
let class = Classification::default();
let literal = serde_json::json!(["Write", "Edit", "MultiEdit"]);
assert_eq!(
value_of(leaf("in_set", "tool.name", literal), &event, &class),
Kleene::True
);
}
#[test]
fn a_json_pointer_reads_into_the_tool_input() {
let event = Event {
tool_input: serde_json::json!({"edits": [{"new_string": "hello"}]}),
..Event::default()
};
let class = Classification::default();
assert_eq!(
value_of(
leaf("equals", "input./edits/0/new_string", "hello".into()),
&event,
&class
),
Kleene::True
);
assert_eq!(
value_of(
leaf("equals", "input./edits/9/new_string", "hello".into()),
&event,
&class
),
Kleene::False
);
}
#[test]
fn input_strings_scans_recursively() {
let event = Event {
tool_input: serde_json::json!({"a": {"b": ["src/deep/nested.rs"]}}),
..Event::default()
};
let class = Classification::default();
assert_eq!(
value_of(
leaf("equals", "input.strings", "src/deep/nested.rs".into()),
&event,
&class
),
Kleene::True
);
}
#[test]
fn keyword_is_case_insensitive_containment() {
let event = Event {
tool_input: serde_json::json!({"note": "the PASSWORD is here"}),
..Event::default()
};
let class = Classification::default();
let literal = serde_json::json!(["password", "secret"]);
assert_eq!(
value_of(leaf("keyword", "input.strings", literal), &event, &class),
Kleene::True
);
}
#[test]
fn keyword_folds_unicode_case_not_just_ascii() {
let event = Event {
tool_input: serde_json::json!({"note": "le CAFÉ est FERMÉ"}),
..Event::default()
};
let class = Classification::default();
assert_eq!(
value_of(
leaf("keyword", "input.strings", serde_json::json!(["café"])),
&event,
&class
),
Kleene::True,
"an ASCII-only fold would miss this, and the rule would not fire"
);
let lower = Event {
tool_input: serde_json::json!({"note": "le café est fermé"}),
..Event::default()
};
assert_eq!(
value_of(
leaf("keyword", "input.strings", serde_json::json!(["CAFÉ"])),
&lower,
&class
),
Kleene::True
);
}
#[test]
fn the_folded_fast_path_and_the_direct_fallback_agree() {
let event = Event {
tool_input: serde_json::json!({"note": "le CAFÉ est FERMÉ"}),
..Event::default()
};
let class = Classification::default();
let set = super::super::facts::FactSet::default();
let tree = node(leaf(
"keyword",
"input.strings",
serde_json::json!(["café", "STRASSE"]),
));
let compiled = ScanTable::compile(&[artifact_of(
serde_json::to_value(&tree).expect("serialises"),
)]);
let empty = ScanTable::default();
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let fast = evaluate_node(&tree, &mut ctx, &compiled);
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let fallback = evaluate_node(&tree, &mut ctx, &empty);
assert_eq!(fast, Kleene::True);
assert_eq!(
fast, fallback,
"the shared automaton and the direct test are one rule, not two"
);
}
#[test]
fn full_case_folding_matches_the_oracle() {
assert_eq!(fold_keyword("STRASSE").as_ref(), "strasse");
assert_eq!(
fold_keyword("straße").as_ref(),
"strasse",
"FULL folding: `ß` becomes `ss`, which `to_lowercase` will not do"
);
assert_eq!(
fold_keyword("straße"),
fold_keyword("STRASSE"),
"the last place this engine and the oracle disagreed on `keyword`"
);
assert_eq!(
fold_keyword("\u{fb01}").as_ref(),
"fi",
"the ligature decomposes under folding, not under lowercasing"
);
for (needle, hay) in [
("café", "CAFÉ"),
("\u{01c6}", "\u{01c4}"),
("σίσυφος", "ΣΊΣΥΦΟΣ"),
] {
assert_eq!(
fold_keyword(needle),
fold_keyword(hay),
"{needle} and {hay} fold together"
);
}
}
#[test]
fn prefix_matches_only_at_offset_zero() {
let event = Event {
tool_input: serde_json::json!({"file_path": "/data/warehouse/x.db"}),
..Event::default()
};
let class = Classification::default();
let literal = serde_json::json!(["/data/", "/lake/"]);
assert_eq!(
value_of(
leaf("prefix", "input./file_path", literal.clone()),
&event,
&class
),
Kleene::True
);
let inner = Event {
tool_input: serde_json::json!({"file_path": "/srv/data/warehouse/x.db"}),
..Event::default()
};
assert_eq!(
value_of(leaf("prefix", "input./file_path", literal), &inner, &class),
Kleene::False,
"a prefix found mid-string is not a prefix"
);
}
#[test]
fn a_single_star_glob_does_not_cross_a_slash() {
let class = Classification {
paths: vec![ClassifiedPath {
class: TargetClass("data_store".to_string()),
value: "/data/warehouse/x.db".to_string(),
}],
..Classification::default()
};
let event = Event::default();
assert_eq!(
value_of(
leaf("glob", "path.value", serde_json::json!(["/data/*"])),
&event,
&class
),
Kleene::False
);
assert_eq!(
value_of(
leaf("glob", "path.value", serde_json::json!(["/data/**"])),
&event,
&class
),
Kleene::True
);
}
#[test]
fn a_brace_in_a_glob_is_a_literal_brace() {
let class = Classification {
paths: vec![ClassifiedPath {
class: TargetClass("data_store".to_string()),
value: "/data/a".to_string(),
}],
..Classification::default()
};
assert_eq!(
value_of(
leaf("glob", "path.value", serde_json::json!(["/data/{a,b}"])),
&Event::default(),
&class
),
Kleene::False
);
}
#[test]
fn regex_lite_searches_unanchored() {
let event = Event {
tool_input: serde_json::json!({"note": "ssn 123-45-6789 here"}),
..Event::default()
};
let class = Classification::default();
let tree = serde_json::json!({
"op": "leaf",
"leaf": {
"pred": "regex_lite",
"field": "input.strings",
"pattern": r"\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b",
},
});
assert_eq!(value_of(tree, &event, &class), Kleene::True);
}
#[test]
fn tld_in_is_membership_over_the_url_tld() {
let class = Classification {
urls: vec![ClassifiedUrl {
value: "https://api.example.com/x".to_string(),
host: "api.example.com".to_string(),
tld: "com".to_string(),
scheme: "https".to_string(),
boundary: UrlBoundary::External,
}],
..Classification::default()
};
assert_eq!(
value_of(
leaf("tld_in", "url.tld", serde_json::json!(["com", "io"])),
&Event::default(),
&class
),
Kleene::True
);
assert_eq!(
value_of(
leaf("tld_in", "url.tld", serde_json::json!(["dev"])),
&Event::default(),
&class
),
Kleene::False
);
}
#[test]
fn int_cmp_covers_all_five_operators() {
let event = Event {
session: Some(SessionFacts {
tool_calls: Some(12),
..SessionFacts::default()
}),
..Event::default()
};
let class = Classification::default();
for (op, n, expected) in [
("lt", 20, Kleene::True),
("le", 12, Kleene::True),
("gt", 5, Kleene::True),
("ge", 12, Kleene::True),
("eq", 12, Kleene::True),
("eq", 99, Kleene::False),
] {
let spec = serde_json::json!({"op": op, "n": n});
assert_eq!(
value_of(leaf("int_cmp", "session.tool_calls", spec), &event, &class),
expected,
"int_cmp {op} {n}"
);
}
}
#[test]
fn int_cmp_refuses_a_boolean() {
let event = Event {
tool_input: serde_json::json!({"flag": true}),
..Event::default()
};
let class = Classification::default();
let spec = serde_json::json!({"op": "ge", "n": 1});
assert_eq!(
value_of(leaf("int_cmp", "input./flag", spec), &event, &class),
Kleene::False
);
}
#[test]
fn exists_is_false_over_an_empty_value_list() {
let class = Classification::default();
let tree = serde_json::json!({
"op": "leaf",
"leaf": {"pred": "exists", "field": "url.host"},
});
assert_eq!(
value_of(tree.clone(), &Event::default(), &class),
Kleene::False
);
let with_url = Classification {
urls: vec![ClassifiedUrl {
value: "https://api.example.com".to_string(),
host: "api.example.com".to_string(),
tld: "com".to_string(),
scheme: "https".to_string(),
boundary: UrlBoundary::External,
}],
..Classification::default()
};
assert_eq!(value_of(tree, &Event::default(), &with_url), Kleene::True);
}
#[test]
fn the_effect_tuple_leaf_matches_any_tuple() {
let class = Classification {
effects: vec![effect("delete", "data_store")],
..Classification::default()
};
let hit = serde_json::json!({
"op": "leaf",
"leaf": {"pred": "effect", "effect": {"verb": "delete", "target_class": "data_store"}},
});
let miss = serde_json::json!({
"op": "leaf",
"leaf": {"pred": "effect", "effect": {"verb": "write", "target_class": "data_store"}},
});
assert_eq!(value_of(hit, &Event::default(), &class), Kleene::True);
assert_eq!(
value_of(miss, &Event::default(), &class),
Kleene::False,
"a wrong verb is a known FALSE, never ⊥"
);
}
#[test]
fn command_and_path_families_read_the_classification() {
let class = Classification {
simple: vec![SimpleCommand {
program: "rm".to_string(),
argv: vec!["-rf".to_string(), "/data/x".to_string()],
raw: "rm -rf /data/x".to_string(),
redirects: Vec::new(),
raw_argv: vec!["-rf".to_string(), "/data/x".to_string()],
}],
paths: vec![ClassifiedPath {
class: TargetClass("secret_material".to_string()),
value: "/home/a/.env".to_string(),
}],
..Classification::default()
};
let event = Event::default();
assert_eq!(
value_of(
leaf("equals", "command.program", "rm".into()),
&event,
&class
),
Kleene::True
);
assert_eq!(
value_of(
leaf(
"in_set",
"command.argv",
serde_json::json!(["-rf", "--force"])
),
&event,
&class
),
Kleene::True
);
assert_eq!(
value_of(
leaf("equals", "path.class", "secret_material".into()),
&event,
&class
),
Kleene::True
);
let simple_exists = serde_json::json!({
"op": "leaf", "leaf": {"pred": "exists", "field": "command.simple"},
});
assert_eq!(value_of(simple_exists, &event, &class), Kleene::True);
}
#[test]
fn agent_type_and_id_read_their_aliases() {
let event = Event {
agent: Some(AgentContext {
agent_id: Some("agent-conformance".to_string()),
agent_type: Some("coding_assistant".to_string()),
..AgentContext::default()
}),
..Event::default()
};
let class = Classification::default();
assert_eq!(
value_of(
leaf("equals", "agent.type", "coding_assistant".into()),
&event,
&class
),
Kleene::True
);
assert_eq!(
value_of(
leaf("equals", "agent.id", "agent-conformance".into()),
&event,
&class
),
Kleene::True
);
}
#[test]
fn an_absent_agent_field_is_false_not_unknown() {
let event = Event {
agent: Some(AgentContext::default()),
..Event::default()
};
let class = Classification::default();
assert_eq!(
value_of(
leaf("equals", "agent.principal", "svc-deployer".into()),
&event,
&class
),
Kleene::False
);
}
#[test]
fn an_absent_session_object_is_false_not_unknown() {
let class = Classification::default();
let spec = serde_json::json!({"op": "ge", "n": 1});
assert_eq!(
value_of(
leaf("int_cmp", "session.tool_calls", spec),
&Event::default(),
&class
),
Kleene::False,
"an absent session is a known negative, not a gap in what we know"
);
}
#[test]
fn exit_code_reads_only_a_post_tool_use_shell_result() {
let class = Classification::default();
let spec = serde_json::json!({"op": "eq", "n": 1});
let tree = leaf("int_cmp", "result.exit_code", spec);
let post_shell = Event {
event_type: "post_tool_use".to_string(),
tool_name: "Bash".to_string(),
tool_result: Some(serde_json::json!({"exit_code": 1})),
..Event::default()
};
assert_eq!(value_of(tree.clone(), &post_shell, &class), Kleene::True);
let pre = Event {
event_type: "pre_tool_use".to_string(),
tool_name: "Bash".to_string(),
tool_result: Some(serde_json::json!({"exit_code": 1})),
..Event::default()
};
assert_eq!(value_of(tree.clone(), &pre, &class), Kleene::False);
let post_read = Event {
event_type: "post_tool_use".to_string(),
tool_name: "Read".to_string(),
tool_result: Some(serde_json::json!({"exit_code": 1})),
..Event::default()
};
assert_eq!(value_of(tree.clone(), &post_read, &class), Kleene::False);
let exists = serde_json::json!({
"op": "leaf", "leaf": {"pred": "exists", "field": "result.exit_code"},
});
assert_eq!(value_of(exists, &pre, &class), Kleene::False);
}
#[test]
fn an_effect_attribute_no_tuple_carries_is_unknown() {
let mut with_attr = effect("write", "data_store");
with_attr
.attrs
.insert("is_production".to_string(), serde_json::json!(true));
let carried = Classification {
effects: vec![with_attr],
..Classification::default()
};
let bare = Classification {
effects: vec![effect("write", "data_store")],
..Classification::default()
};
let tree = leaf("equals", "effect.attrs.is_production", true.into());
assert_eq!(
value_of(tree.clone(), &Event::default(), &carried),
Kleene::True
);
assert_eq!(
value_of(tree, &Event::default(), &bare),
Kleene::UNKNOWN,
"no tuple said whether this was production — that is a gap, not a no"
);
let mut false_attr = effect("write", "data_store");
false_attr
.attrs
.insert("is_production".to_string(), serde_json::json!(false));
let denied = Classification {
effects: vec![false_attr],
..Classification::default()
};
assert_eq!(
value_of(
leaf("equals", "effect.attrs.is_production", true.into()),
&Event::default(),
&denied
),
Kleene::False,
"a tuple that said `false` is a known negative"
);
}
#[test]
fn one_shared_scan_result_feeds_n_trees_identically_to_n_independent_scans() {
let trees = [
node(leaf(
"keyword",
"input.strings",
serde_json::json!(["password", "secret"]),
)),
node(leaf(
"prefix",
"input./file_path",
serde_json::json!(["/data/", "/lake/"]),
)),
node(serde_json::json!({
"op": "leaf",
"leaf": {"pred": "regex_lite", "field": "input.strings",
"pattern": r"[0-9]{3}-[0-9]{2}-[0-9]{4}"},
})),
node(leaf("glob", "path.value", serde_json::json!(["/data/**"]))),
];
let artifacts: Vec<LoadedArtifact> = trees
.iter()
.map(|tree| artifact_of(serde_json::to_value(tree).expect("serialises")))
.collect();
let table = ScanTable::compile(&artifacts);
assert_eq!(table.scanned_fields.len(), 3, "three surfaces, not one");
let event = Event {
tool_input: serde_json::json!({
"file_path": "/data/warehouse/x.db",
"note": "the PASSWORD is 123-45-6789",
}),
..Event::default()
};
let class = Classification {
paths: vec![ClassifiedPath {
class: TargetClass("data_store".to_string()),
value: "/data/warehouse/x.db".to_string(),
}],
..Classification::default()
};
let set = super::super::facts::FactSet::default();
let independent: Vec<Kleene> = trees
.iter()
.map(|tree| {
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
evaluate_node(tree, &mut ctx, &table)
})
.collect();
let shared_result = {
let ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
table.scan(&ctx)
};
let shared: Vec<Kleene> = trees
.iter()
.map(|tree| {
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
evaluate_node_with(tree, &mut ctx, &table, &shared_result)
})
.collect();
assert_eq!(independent, shared);
assert_eq!(
independent,
vec![Kleene::True, Kleene::True, Kleene::True, Kleene::True],
"and all four actually fired, so the comparison is not vacuous"
);
}
#[test]
fn the_scan_surface_is_per_field_not_one_concatenated_haystack() {
let over_result = node(leaf(
"keyword",
"result.strings",
serde_json::json!(["password"]),
));
let over_input = node(leaf(
"keyword",
"input.strings",
serde_json::json!(["password"]),
));
let table = ScanTable::compile(&[
artifact_of(serde_json::to_value(&over_result).expect("serialises")),
artifact_of(serde_json::to_value(&over_input).expect("serialises")),
]);
assert_eq!(
table.keyword_sources.len(),
1,
"the same source compiles once and is shared by both trees"
);
let event = Event {
tool_input: serde_json::json!({"note": "password"}),
tool_result: Some(serde_json::json!({"stdout": "all clear"})),
..Event::default()
};
let class = Classification::default();
let set = super::super::facts::FactSet::default();
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let result = table.scan(&ctx);
assert_eq!(
evaluate_node_with(&over_input, &mut ctx, &table, &result),
Kleene::True
);
assert_eq!(
evaluate_node_with(&over_result, &mut ctx, &table, &result),
Kleene::False,
"the keyword is in the input, not in the result"
);
}
#[test]
fn every_regex_constructor_explicitly_sizes_its_cache_pool() {
let sources = vec![r"\w+".to_string(), r"\d+".to_string()];
let globs = vec![glob_to_regex("*"), glob_to_regex("?")];
for re in [
compile_one_regex(&sources[0]).unwrap(),
compile_regex_set(&sources).unwrap(),
compile_one_glob("*").unwrap(),
compile_pattern_set(&globs, true).unwrap(),
] {
let inherited = meta::Regex::builder()
.configure(meta::Regex::config().pool_capacity(1))
.configure(re.get_config().clone())
.build("")
.unwrap();
assert_eq!(inherited.get_config().get_pool_capacity(), 8);
}
}
#[test]
fn pattern_sets_preserve_bytes_unicode_and_every_matching_source_id() {
let regexes: Vec<String> = [r"", r"\w+", r"(?u:\w+)", r"\xFF", r"a|ab"]
.map(str::to_string)
.into();
let set = compile_regex_set(®exes).unwrap();
for (text, expected) in [
(b"ab".as_slice(), vec![0, 1, 2, 4]),
("雪".as_bytes(), vec![0, 2]),
(b"\xFF".as_slice(), vec![0, 3]),
(b"".as_slice(), vec![0]),
] {
let actual: Vec<usize> = matching_patterns(&set, text)
.iter()
.map(|id| id.as_usize())
.collect();
assert_eq!(actual, expected);
for (id, source) in regexes.iter().enumerate() {
assert_eq!(
compile_one_regex(source).unwrap().is_match(text),
expected.contains(&id),
);
}
}
let globs = ["**", "?", "*", "a*", "a?", "[a]", "{a,b}"];
let translated = globs.map(glob_to_regex);
let set = compile_pattern_set(&translated, true).unwrap();
for (text, expected) in [
("é", vec![0, 1, 2]),
("a/b", vec![0]),
("a\n", vec![0, 2, 3, 4]),
("[a]", vec![0, 2, 5]),
("{a,b}", vec![0, 2, 6]),
("", vec![0, 2]),
] {
let actual: Vec<usize> = matching_patterns(&set, text.as_bytes())
.iter()
.map(|id| id.as_usize())
.collect();
assert_eq!(actual, expected);
for (id, source) in globs.iter().enumerate() {
assert_eq!(
compile_one_glob(source).unwrap().is_match(text),
expected.contains(&id),
);
}
}
}
#[test]
fn regex_lite_runs_in_ascii_mode() {
let event = Event {
tool_input: serde_json::json!({"word": "naïve"}),
..Event::default()
};
let class = Classification::default();
let tree = serde_json::json!({
"op": "leaf",
"leaf": {"pred": "regex_lite", "field": "input./word", "pattern": r"^\w+$"},
});
assert_eq!(value_of(tree, &event, &class), Kleene::False);
let ascii = Event {
tool_input: serde_json::json!({"word": "naive"}),
..Event::default()
};
let control = serde_json::json!({
"op": "leaf",
"leaf": {"pred": "regex_lite", "field": "input./word", "pattern": r"^\w+$"},
});
assert_eq!(value_of(control, &ascii, &class), Kleene::True);
}
#[test]
fn a_regex_over_the_source_limit_skips_its_artifact_and_leaves_the_others_active() {
let oversized = "a".repeat(REGEX_SOURCE_LIMIT + 1);
let doc = serde_json::json!({
"schema_version": 2,
"organization_id": "org",
"revision": 1,
"built_at": "2026-09-01T00:00:00Z",
"enforcement_enabled": true,
"signature": null,
"artifacts": [
{
"artifact_id": "big",
"kind": "t1_predicate_tree",
"body": {
"node": {"op": "leaf", "leaf": {
"pred": "regex_lite", "field": "input.strings", "pattern": oversized,
}},
"verdict": "block", "reason": "r",
},
},
{
"artifact_id": "small",
"kind": "t1_predicate_tree",
"body": {
"node": {"op": "leaf", "leaf": {
"pred": "equals", "field": "tool.name", "value": "Bash",
}},
"verdict": "block", "reason": "r",
},
},
],
});
let bundle = super::super::bundle::load(doc).expect("one bad pattern is not a bad bundle");
assert_eq!(bundle.artifacts.len(), 1, "the other artifact stayed armed");
assert_eq!(bundle.artifacts[0].artifact_id(), Some("small"));
assert_eq!(bundle.skipped.len(), 1);
assert_eq!(bundle.skipped[0].id, "big");
assert_eq!(
bundle.skipped[0].reason,
super::super::types::SKIP_BAD_PATTERN
);
}
#[test]
fn the_corpus_size_limit_row_is_about_the_source_cap_not_the_memory_guard() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("schemas/conformance");
let document: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(
dir.join("bundles/07-parse-regex-lite-over-the-size-limit.json"),
)
.expect("the fixture is readable"),
)
.expect("the fixture is valid JSON");
let source = document["artifacts"][0]["body"]["node"]["leaf"]["pattern"]
.as_str()
.expect("the fixture carries a pattern");
assert!(
source.len() > REGEX_SOURCE_LIMIT,
"the fixture is over the source cap: {} bytes",
source.len()
);
assert!(
compile_one_regex(source).is_some(),
"and the MEMORY GUARD would have let it through — which is the whole \
point of the row: the source cap is what rejects it"
);
let bundle =
super::super::bundle::load(document).expect("one bad pattern is not a bad bundle");
let skipped: Vec<&str> = bundle.skipped.iter().map(|s| s.id.as_str()).collect();
assert_eq!(skipped, vec!["bad"], "the oversize artifact is skipped");
assert_eq!(
bundle.skipped[0].reason,
super::super::types::SKIP_BAD_PATTERN
);
assert_eq!(
bundle
.artifacts
.iter()
.filter_map(|a| a.artifact_id())
.collect::<Vec<_>>(),
vec!["good"],
"and the rest of the bundle stays armed"
);
}
#[test]
fn the_memory_guard_is_a_last_resort_and_never_decides_before_the_source_cap() {
for ordinary in [r"\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b", "[a-z]{5000}"] {
assert!(
compile_one_regex(ordinary).is_some(),
"the guard must not fire on {ordinary:?}"
);
}
let bomb = "(?:a{1000}){1000}";
assert!(
bomb.len() < REGEX_SOURCE_LIMIT,
"the source cap does not catch it"
);
assert!(compile_one_regex(bomb).is_none(), "the guard does");
}
#[test]
fn look_around_and_back_references_are_not_expressible_and_skip_their_artifact() {
for unsupported in ["(?=secret)x", r"(a)\1"] {
assert!(
validate_patterns(Some(&node(serde_json::json!({
"op": "leaf",
"leaf": {"pred": "regex_lite", "field": "input.strings", "pattern": unsupported},
}))))
.is_err(),
"{unsupported:?} is not expressible in (?-u) mode"
);
}
}
#[test]
fn validation_closes_the_operator_predicate_and_field_sets() {
assert!(validate_node(None).is_err());
assert!(validate_node(Some(&node(serde_json::json!({
"op": "xor", "children": [],
}))))
.is_err());
assert!(validate_node(Some(&node(serde_json::json!({
"op": "leaf", "leaf": {"pred": "sounds_like", "field": "tool.name", "value": "x"},
}))))
.is_err());
assert!(validate_node(Some(&node(serde_json::json!({
"op": "leaf", "leaf": {"pred": "equals", "field": "tool.nombre", "value": "x"},
}))))
.is_err());
assert!(validate_node(Some(&node(serde_json::json!({
"op": "leaf", "leaf": {"pred": "fact", "fact": {"fact_id": "x", "op": "matches"}},
}))))
.is_err());
assert!(validate_node(Some(&node(
serde_json::json!({"op": "and", "children": []})
)))
.is_err());
assert!(validate_node(Some(&node(serde_json::json!({
"op": "not",
"children": [
{"op": "leaf", "leaf": {"pred": "equals", "field": "tool.name", "value": "a"}},
{"op": "leaf", "leaf": {"pred": "equals", "field": "tool.name", "value": "b"}},
],
}))))
.is_err());
for field in ["input./command", "fact.change_ticket", "effect.attrs.force"] {
assert!(
validate_node(Some(&node(serde_json::json!({
"op": "leaf", "leaf": {"pred": "exists", "field": field},
}))))
.is_ok(),
"{field} is in the vocabulary"
);
}
}
fn unknown_leaf() -> serde_json::Value {
leaf("equals", "effect.attrs.is_production", true.into())
}
fn truth(value: bool) -> serde_json::Value {
leaf(
"equals",
"tool.name",
if value { "Bash" } else { "Read" }.into(),
)
}
#[test]
fn an_unknown_propagates_through_and_or_and_not_without_collapsing() {
let event = bash("rm -rf /data/x");
let class = Classification {
effects: vec![effect("delete", "data_store")],
..Classification::default()
};
assert_eq!(
value_of(unknown_leaf(), &event, &class),
Kleene::UNKNOWN,
"the fixture leaf really is ⊥"
);
let and_true = serde_json::json!({"op": "and", "children": [truth(true), unknown_leaf()]});
let and_false =
serde_json::json!({"op": "and", "children": [truth(false), unknown_leaf()]});
assert_eq!(value_of(and_true, &event, &class), Kleene::UNKNOWN);
assert_eq!(value_of(and_false, &event, &class), Kleene::False);
let or_true = serde_json::json!({"op": "or", "children": [truth(true), unknown_leaf()]});
let or_false = serde_json::json!({"op": "or", "children": [truth(false), unknown_leaf()]});
assert_eq!(value_of(or_true, &event, &class), Kleene::True);
assert_eq!(value_of(or_false, &event, &class), Kleene::UNKNOWN);
let negated = serde_json::json!({"op": "not", "children": [unknown_leaf()]});
assert_eq!(value_of(negated, &event, &class), Kleene::UNKNOWN);
}
#[test]
fn the_node_ops_are_two_valued_when_nothing_is_unknown() {
let event = bash("rm -rf /data/x");
let class = Classification {
effects: vec![effect("delete", "data_store")],
..Classification::default()
};
let and_both = serde_json::json!({"op": "and", "children": [truth(true), truth(true)]});
let and_one = serde_json::json!({"op": "and", "children": [truth(true), truth(false)]});
let or_second = serde_json::json!({"op": "or", "children": [truth(false), truth(true)]});
let or_neither = serde_json::json!({"op": "or", "children": [truth(false), truth(false)]});
assert_eq!(value_of(and_both, &event, &class), Kleene::True);
assert_eq!(value_of(and_one, &event, &class), Kleene::False);
assert_eq!(value_of(or_second, &event, &class), Kleene::True);
assert_eq!(value_of(or_neither, &event, &class), Kleene::False);
assert_eq!(
value_of(
serde_json::json!({"op": "not", "children": [truth(false)]}),
&event,
&class
),
Kleene::True
);
}
#[test]
fn every_child_is_evaluated_so_a_bottom_still_reaches_the_report() {
let event = Event::default();
let class = Classification::default();
let set = super::super::facts::FactSet::default();
let tree = serde_json::json!({
"op": "and",
"children": [
truth(false),
{"op": "leaf", "leaf": {"pred": "fact",
"fact": {"fact_id": "change_ticket", "op": "equals",
"value": true}}},
],
});
let (value, inconclusive) = run(tree, &event, &class, &set);
assert_eq!(value, Kleene::False, "the verdict is unchanged");
assert_eq!(
inconclusive,
vec!["change_ticket".to_string()],
"and the gap is still reported"
);
}
#[test]
fn the_corpus_pins_every_leaf_as_a_truth_value() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("schemas/conformance");
let rows = std::fs::read_to_string(dir.join("01-tier1-preds.jsonl"))
.expect("the corpus file is readable");
let mut checked = 0usize;
let mut disagreements: Vec<String> = Vec::new();
for line in rows.lines().filter(|l| !l.trim().is_empty()) {
let row: serde_json::Value =
serde_json::from_str(line).expect("every corpus row is valid JSON");
let id = row["id"].as_str().unwrap_or_default().to_string();
let bundle_ref = row["bundle_ref"]
.as_str()
.expect("every row names a bundle");
let document: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(dir.join(bundle_ref)).expect("the bundle is readable"),
)
.expect("the bundle is valid JSON");
let bundle = super::super::bundle::load(document).expect("the bundle loads");
let Some(artifact) = bundle.artifacts.first() else {
continue; };
let ArtifactBody::T1(body) = &artifact.body else {
continue;
};
let Some(tree) = body.node.as_ref() else {
continue;
};
let event: Event =
serde_json::from_value(row["event"].clone()).expect("the event deserialises");
let now_ms = row["now_ms"].as_i64().expect("every row carries now_ms");
let facts = super::super::facts::resolve(&bundle, now_ms);
let class =
super::super::effect::classify(&event, &facts, bundle.effect_classes.as_ref());
let mut ctx = EvalContext::new(&event, &class, &facts, now_ms);
let value = evaluate_node(tree, &mut ctx, &bundle.scan);
let decision = &row["expected"]["decision"];
let expected = match decision["verdict"].as_str() {
Some("block") => Kleene::True,
Some("ask") => Kleene::UNKNOWN,
Some("allow") if decision["undecided"].as_bool() == Some(true) => Kleene::False,
_ => continue,
};
checked += 1;
let agrees = match (expected, value) {
(Kleene::Unknown(_), Kleene::Unknown(_)) => true,
(left, right) => left == right,
};
if !agrees {
disagreements.push(format!("{id}: expected {expected:?}, got {value:?}"));
}
}
assert!(
checked >= 70,
"the corpus was found and read: {checked} rows"
);
assert!(
disagreements.is_empty(),
"{} of {checked} corpus leaves disagree with Tier 1 - the engine is wrong \
until the spec says the row was:\n {}",
disagreements.len(),
disagreements.join("\n ")
);
}
#[test]
fn a_false_tree_contributes_nothing() {
let artifact = artifact_of(leaf("equals", "tool.name", "Bash".into()));
let ArtifactBody::T1(body) = &artifact.body else {
panic!("the fixture is a t1 artifact");
};
let event = Event {
tool_name: "Read".to_string(),
..Event::default()
};
let class = Classification::default();
let set = super::super::facts::FactSet::default();
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let table = ScanTable::compile(std::slice::from_ref(&artifact));
assert!(contribution(&artifact, body, &mut ctx, &table).is_none());
}
#[test]
fn a_true_tree_contributes_the_bodys_verdict_and_the_envelopes_identity() {
let artifact = artifact_of(leaf("equals", "tool.name", "Bash".into()));
let ArtifactBody::T1(body) = &artifact.body else {
panic!("the fixture is a t1 artifact");
};
let event = bash("rm -rf /data/x");
let class = Classification::default();
let set = super::super::facts::FactSet::default();
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let table = ScanTable::compile(std::slice::from_ref(&artifact));
let contribution =
contribution(&artifact, body, &mut ctx, &table).expect("a true tree fires");
assert_eq!(contribution.verdict, Verdict::Block);
assert_eq!(contribution.reason, "probe fired");
assert_eq!(contribution.artifact_id.as_deref(), Some("probe"));
assert_eq!(contribution.atom_id.as_deref(), Some("atom-probe"));
assert_eq!(contribution.tier, Some(1));
assert!(contribution.inconclusive.is_empty());
}
#[test]
fn an_unknown_tree_takes_the_artifacts_on_inconclusive_branch() {
let artifact = artifact_of(unknown_leaf());
let ArtifactBody::T1(body) = &artifact.body else {
panic!("the fixture is a t1 artifact");
};
let event = Event::default();
let class = Classification {
effects: vec![effect("delete", "data_store")],
..Classification::default()
};
let set = super::super::facts::FactSet::default();
let mut ctx = EvalContext::new(&event, &class, &set, 1_756_742_400_000);
let table = ScanTable::compile(std::slice::from_ref(&artifact));
let contribution = contribution(&artifact, body, &mut ctx, &table)
.expect("⊥ contributes, it does not fire");
assert_eq!(
contribution.verdict,
Verdict::Ask,
"the fixture declares on_inconclusive: ask"
);
}
}