use crate::*;
pub(crate) use candor_report::GateViolation;
#[allow(clippy::too_many_arguments)]
pub(crate) fn policy_violations(
policy_text: &str,
all: &[String],
inferred: &HashMap<String, BTreeSet<&'static str>>,
calls: &HashMap<String, BTreeSet<String>>,
hostsacc: &HashMap<String, BTreeSet<String>>,
cmdsacc: &HashMap<String, BTreeSet<String>>,
pathsacc: &HashMap<String, BTreeSet<String>>,
tablesacc: &HashMap<String, BTreeSet<String>>,
incompleteacc: &HashMap<String, BTreeSet<&'static str>>,
) -> Vec<GateViolation> {
use candor_classify::policy::{literal_allowed, parse_policy, scope_matches};
let p = parse_policy(policy_text);
let empty: BTreeSet<&'static str> = BTreeSet::new();
let mut out = Vec::new();
for q in all {
let inf = inferred.get(q).unwrap_or(&empty);
for r in &p.rules {
if let Some(s) = &r.scope {
if !scope_matches(q, s) {
continue;
}
}
let hits: Vec<&str> = if r.effects.is_empty() {
inf.iter().copied().filter(|e| *e != "Unknown").collect()
} else {
inf.iter().copied().filter(|e| r.effects.contains(e)).collect()
};
if !hits.is_empty() {
out.push(GateViolation {
rule: "AS-EFF-006".into(),
func: q.clone(),
effects: hits.iter().map(|s| s.to_string()).collect(),
detail: format!("`{q}` performs {{ {} }}, forbidden by policy: `{}`", hits.join(", "), r.raw),
});
}
}
for r in &p.allow_rules {
if let Some(s) = &r.scope {
if !scope_matches(q, s) {
continue;
}
}
if !inf.contains(r.effect) {
continue;
}
let lits = match r.effect {
"Net" => hostsacc.get(q),
"Exec" => cmdsacc.get(q),
"Db" => tablesacc.get(q),
_ => pathsacc.get(q),
};
let surface_incomplete = incompleteacc.get(q).is_some_and(|s| s.contains(r.effect));
match lits {
Some(ls) if !ls.is_empty() && !surface_incomplete => {
let bad: Vec<&str> =
ls.iter().filter(|l| !literal_allowed(r.effect, l, &r.literals)).map(String::as_str).collect();
if !bad.is_empty() {
out.push(GateViolation {
rule: "AS-EFF-008".into(),
func: q.clone(),
effects: vec![r.effect.to_string()],
detail: format!("`{q}` reaches {{ {} }} outside the allowlist: `{}`", bad.join(", "), r.raw),
});
}
}
_ => out.push(GateViolation {
rule: "AS-EFF-008".into(),
func: q.clone(),
effects: vec![r.effect.to_string()],
detail: format!("`{q}` performs {} with no visible literal — the surface cannot be certified: `{}`", r.effect, r.raw),
}),
}
}
for r in &p.layer_rules {
if !scope_matches(q, &r.from) {
continue;
}
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut stack: Vec<&str> = calls.get(q).map(|cs| cs.iter().map(String::as_str).collect()).unwrap_or_default();
let mut hit: Option<&str> = None;
while let Some(n) = stack.pop() {
if !seen.insert(n) {
continue;
}
if scope_matches(n, &r.to) {
hit = Some(n);
break;
}
if let Some(cs) = calls.get(n) {
stack.extend(cs.iter().map(String::as_str));
}
}
if let Some(h) = hit {
out.push(GateViolation {
rule: "AS-EFF-009".into(),
func: q.clone(),
effects: Vec::new(), detail: format!("`{q}` reaches into a forbidden layer (via `{h}`): `{}`", r.raw),
});
}
}
}
out.sort_by(|a, b| (a.rule.as_str(), a.detail.as_str()).cmp(&(b.rule.as_str(), b.detail.as_str())));
out
}
pub(crate) static GATE_JSON_PATH: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
pub(crate) static GATE_VIOLATIONS: std::sync::OnceLock<std::sync::Mutex<Vec<GateViolation>>> = std::sync::OnceLock::new();
pub(crate) fn record_gate_violations(violations: &[GateViolation]) {
if !matches!(GATE_JSON_PATH.get(), Some(Some(_))) {
return;
}
let acc = GATE_VIOLATIONS.get_or_init(|| std::sync::Mutex::new(Vec::new()));
acc.lock().unwrap().extend(violations.iter().cloned());
}
pub(crate) fn write_gate_json(exit_code: i32) {
let Some(Some(path)) = GATE_JSON_PATH.get() else { return };
if exit_code == 2 {
eprintln!("candor-scan: --gate-json not written — the scan/gate did not complete (exit 2)");
return;
}
let acc = GATE_VIOLATIONS.get_or_init(|| std::sync::Mutex::new(Vec::new()));
let mut violations = acc.lock().unwrap().clone();
match candor_report::gate_verdict_json(&mut violations) {
Ok(json) if path == "-" => println!("{json}"),
Ok(json) => {
if let Err(e) = candor_report::write_atomic(std::path::Path::new(path), format!("{json}\n").as_bytes()) {
eprintln!("candor-scan: could not write --gate-json {path}: {e}");
}
}
Err(e) => eprintln!("candor-scan: could not serialize gate verdict: {e}"),
}
}
pub(crate) fn host_part(h: &str) -> String {
let a = h.split_once("://").map(|(_, r)| r).unwrap_or(h);
let a = a.split('/').next().unwrap_or(a);
a.rsplit_once('@').map(|(_, h)| h).unwrap_or(a).to_string()
}