use crate::generated::types::{T3Hold, Verdict};
use super::bundle::LoadedArtifact;
use super::kleene::Kleene;
use super::tier1::ScanTable;
use super::tier2::{build, declared_mode, on_inconclusive_verdict};
use super::types::{Contribution, EvalContext, HoldRequest};
pub fn validate_hold(body: &T3Hold) -> Result<(), String> {
super::tier1::validate_node(body.trigger.as_ref())
}
pub fn contribution(
artifact: &LoadedArtifact,
body: &T3Hold,
ctx: &mut EvalContext<'_>,
scan: &ScanTable,
) -> Option<Contribution> {
let trigger = body.trigger.as_ref()?;
let mode = declared_mode(artifact);
let reason = body.reason.clone().unwrap_or_default();
let mut child = ctx.fork();
let value = super::tier1::evaluate_node(trigger, &mut child, scan);
ctx.merge_warnings(&child);
match value {
Kleene::False => None,
Kleene::Unknown(_) => Some(build(
artifact,
mode.clone(),
on_inconclusive_verdict(artifact, &mode),
reason,
child.inconclusive,
Vec::new(),
)),
Kleene::True => {
let mut contribution =
build(artifact, mode, Verdict::Ask, reason, Vec::new(), Vec::new());
contribution.hold = Some(HoldRequest {
resolve: body.resolve.clone(),
directive_template_id: body.directive_template_id.clone(),
max_attempts: body.max_attempts,
timeout_s: body.timeout_s,
on_timeout: body.on_timeout,
verdict_on_approve: body.verdict_on_approve,
verdict_on_reject: body.verdict_on_reject,
});
Some(contribution)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::types::{PolicyArtifact, T1Node};
use crate::zone_eval::bundle::ArtifactBody;
use crate::zone_eval::facts::FactSet;
use crate::zone_eval::types::{Classification, Effect, Event, MODE_MONITOR};
const NOW: i64 = 1_756_742_400_000;
const REASON: &str = "a force-push to a protected branch cannot be undone";
fn node(value: serde_json::Value) -> T1Node {
serde_json::from_value(value).expect("the trigger parses")
}
fn vcs_trigger() -> T1Node {
node(serde_json::json!({
"op": "leaf",
"leaf": {"pred": "effect", "effect": {"verb": "delete", "target_class": "vcs_remote"}}
}))
}
fn unresolvable_trigger() -> T1Node {
node(serde_json::json!({
"op": "leaf",
"leaf": {"pred": "fact", "fact": {"fact_id": "change_ticket", "op": "equals", "value": true}}
}))
}
fn hold_body(trigger: Option<T1Node>) -> T3Hold {
T3Hold {
trigger,
reason: Some(REASON.to_string()),
resolve: Some("ask_human".to_string()),
directive_template_id: Some("tmpl-vcs-hold".to_string()),
max_attempts: Some(2),
timeout_s: Some(120),
on_timeout: Some(Verdict::Block),
verdict_on_approve: Some(Verdict::Allow),
verdict_on_reject: Some(Verdict::Block),
}
}
fn artifact(mode: &str, on_inconclusive: &str) -> LoadedArtifact {
LoadedArtifact {
envelope: PolicyArtifact {
artifact_id: Some("sa2".to_string()),
atom_id: Some("atom-sa2".to_string()),
tier: Some(3),
mode: Some(mode.to_string().into()),
on_inconclusive: Some(on_inconclusive.to_string().into()),
..Default::default()
},
body: ArtifactBody::T3(Box::new(hold_body(Some(vcs_trigger())))),
}
}
fn fired() -> Classification {
Classification {
effects: vec![Effect {
verb: "delete".to_string().into(),
target_class: "vcs_remote".to_string().into(),
attrs: serde_json::Map::new(),
}],
..Default::default()
}
}
fn evaluate(
artifact: &LoadedArtifact,
body: &T3Hold,
classification: &Classification,
) -> Option<Contribution> {
let event = Event {
event_type: "pre_tool_use".to_string(),
tool_name: "Bash".to_string(),
tool_input: serde_json::json!({"command": "git push --force origin main"}),
..Default::default()
};
let facts = FactSet::default();
let mut ctx = EvalContext::new(&event, classification, &facts, NOW);
contribution(artifact, body, &mut ctx, &ScanTable::default())
}
#[test]
fn a_hold_with_no_trigger_contributes_nothing() {
let artifact = artifact("enforce", "ask");
assert!(evaluate(&artifact, &hold_body(None), &fired()).is_none());
}
#[test]
fn a_false_trigger_contributes_nothing() {
let artifact = artifact("enforce", "ask");
let body = hold_body(Some(vcs_trigger()));
assert!(evaluate(&artifact, &body, &Classification::default()).is_none());
}
#[test]
fn a_matching_trigger_opens_a_hold_and_contributes_ask() {
let artifact = artifact("enforce", "ask");
let body = hold_body(Some(vcs_trigger()));
let contribution = evaluate(&artifact, &body, &fired()).expect("the trigger matched");
assert_eq!(contribution.verdict, Verdict::Ask);
let hold = contribution.hold.expect("a fired trigger opens a hold");
assert_eq!(hold.resolve.as_deref(), Some("ask_human"));
assert_eq!(hold.directive_template_id.as_deref(), Some("tmpl-vcs-hold"));
assert_eq!(hold.max_attempts, Some(2));
assert_eq!(hold.timeout_s, Some(120));
assert_eq!(hold.on_timeout, Some(Verdict::Block));
assert_eq!(hold.verdict_on_approve, Some(Verdict::Allow));
assert_eq!(hold.verdict_on_reject, Some(Verdict::Block));
}
#[test]
fn the_trigger_decision_is_all_this_returns() {
let artifact = artifact("enforce", "ask");
let body = hold_body(Some(vcs_trigger()));
let first = evaluate(&artifact, &body, &fired()).expect("fired");
let second = evaluate(&artifact, &body, &fired()).expect("fired again");
assert_eq!(first, second, "nothing was consumed by the first call");
}
#[test]
fn an_inconclusive_trigger_takes_the_artifacts_on_inconclusive_and_opens_no_hold() {
let artifact = artifact("enforce", "block");
let body = hold_body(Some(unresolvable_trigger()));
let contribution = evaluate(&artifact, &body, &fired()).expect("⊥ still contributes");
assert_eq!(contribution.verdict, Verdict::Block);
assert_eq!(contribution.inconclusive, vec!["change_ticket".to_string()]);
assert!(
contribution.hold.is_none(),
"an undecided trigger opens no hold — there is nothing to ask about yet"
);
}
#[test]
fn a_monitor_hold_is_always_allow_and_flag_when_inconclusive() {
let artifact = artifact(MODE_MONITOR, "block");
let body = hold_body(Some(unresolvable_trigger()));
let contribution = evaluate(&artifact, &body, &fired()).expect("monitor still contributes");
assert_eq!(
contribution.verdict,
Verdict::Allow,
"monitor never blocks, not even on a fact it could not read"
);
assert_eq!(contribution.mode.as_str(), MODE_MONITOR);
}
#[test]
fn a_monitor_hold_still_reports_its_hold_and_the_join_is_what_drops_it() {
let artifact = artifact(MODE_MONITOR, "ask");
let body = hold_body(Some(vcs_trigger()));
let contribution = evaluate(&artifact, &body, &fired()).expect("fired");
assert!(contribution.hold.is_some());
assert!(!contribution.is_enforcing());
}
#[test]
fn the_reason_is_the_bodys_and_reaches_the_developer_verbatim() {
let artifact = artifact("enforce", "ask");
let body = hold_body(Some(vcs_trigger()));
let contribution = evaluate(&artifact, &body, &fired()).expect("fired");
assert_eq!(contribution.reason, REASON);
}
#[test]
fn validate_hold_rejects_a_body_with_no_trigger() {
assert!(validate_hold(&hold_body(None)).is_err());
assert!(validate_hold(&hold_body(Some(vcs_trigger()))).is_ok());
}
}