use crate::config::{Alias, OverrideOp, Policy, Requirement, SubjectMatcher};
use crate::expr::{self, Expr, Tri};
use openvet_crypto::TaggedHash;
use openvet_proto::{Subject, audit::Audit};
use std::fmt;
#[derive(Debug, Clone)]
pub enum Verdict {
Pass,
Fail(Vec<FailureReason>),
Unaudited,
}
#[derive(Debug, Clone)]
pub struct FailureReason {
pub requirement: String,
pub kind: FailureKind,
}
#[derive(Debug, Clone)]
pub enum FailureKind {
NotAsserted,
Contradicted(Vec<AuditContradiction>),
}
#[derive(Debug, Clone)]
pub struct AuditContradiction {
pub log: String,
pub relevant_claims: Vec<(String, Tri)>,
}
pub fn evaluate(policy: &Policy, subject: &Subject, audits: &[(&str, &Audit)]) -> Verdict {
let reqs = effective_requirements(policy, subject);
if audits.is_empty() && !reqs.is_empty() {
return Verdict::Unaudited;
}
let mut failures = Vec::new();
for name in reqs {
let Some(req) = policy.requirement(&name) else {
continue;
};
if let Some(kind) = evaluate_requirement(policy, req, audits) {
failures.push(FailureReason {
requirement: name,
kind,
});
}
}
if failures.is_empty() {
Verdict::Pass
} else {
Verdict::Fail(failures)
}
}
pub fn effective_requirements(policy: &Policy, subject: &Subject) -> Vec<String> {
let mut current: Vec<String> = policy
.requirements
.iter()
.filter(|r| r.default)
.map(|r| r.name.clone())
.collect();
for ov in &policy.overrides {
if !matches_subject(&ov.matcher, subject) {
continue;
}
match &ov.op {
OverrideOp::Replace(names) => current = names.clone(),
OverrideOp::Patch { add, remove } => {
current.retain(|n| !remove.contains(n));
for a in add {
if !current.contains(a) {
current.push(a.clone());
}
}
}
}
}
current
}
fn evaluate_requirement(
policy: &Policy,
req: &Requirement,
audits: &[(&str, &Audit)],
) -> Option<FailureKind> {
let mut some_true = false;
let mut contradictions: Vec<AuditContradiction> = Vec::new();
for (log, audit) in audits {
let lookup = |name: &str| resolve_claim(audit, log, name, &policy.aliases);
match expr::evaluate(&req.expr, &lookup) {
Tri::True => some_true = true,
Tri::False => {
contradictions.push(AuditContradiction {
log: (*log).to_string(),
relevant_claims: claim_snapshot(&req.expr, &lookup),
});
}
Tri::Unknown => {}
}
}
if !contradictions.is_empty() {
Some(FailureKind::Contradicted(contradictions))
} else if some_true {
None
} else {
Some(FailureKind::NotAsserted)
}
}
pub fn claim_lookup<'a>(
log: &'a str,
audit: &'a Audit,
aliases: &'a [Alias],
) -> impl Fn(&str) -> Tri + 'a {
move |name: &str| resolve_claim(audit, log, name, aliases)
}
fn resolve_claim(audit: &Audit, log: &str, canonical: &str, aliases: &[Alias]) -> Tri {
let actual = aliases
.iter()
.find(|a| a.canonical == canonical)
.and_then(|a| a.mappings.iter().find(|(l, _)| l == log))
.map(|(_, n)| n.as_str())
.unwrap_or(canonical);
match audit.claims.get(actual) {
Some(true) => Tri::True,
Some(false) => Tri::False,
None => Tri::Unknown,
}
}
fn claim_snapshot<F>(expr: &Expr, lookup: &F) -> Vec<(String, Tri)>
where
F: Fn(&str) -> Tri,
{
let mut names = Vec::new();
collect_claims(expr, &mut names);
names
.into_iter()
.map(|n| {
let v = lookup(&n);
(n, v)
})
.collect()
}
fn collect_claims(expr: &Expr, out: &mut Vec<String>) {
match expr {
Expr::Claim(name) => {
if !out.iter().any(|n| n == name) {
out.push(name.clone());
}
}
Expr::Not(inner) => collect_claims(inner, out),
Expr::And(children) | Expr::Or(children) => {
for c in children {
collect_claims(c, out);
}
}
}
}
fn matches_subject(m: &SubjectMatcher, s: &Subject) -> bool {
matches_str(&m.registry, &s.registry)
&& matches_str(&m.package, &s.package)
&& matches_str(&m.version, &s.version)
&& matches_str(&m.variant, s.variant.as_deref().unwrap_or(""))
&& matches_hash(&m.hash, &s.hash)
}
fn matches_str(matcher: &Option<String>, value: &str) -> bool {
match matcher.as_deref() {
None | Some("*") => true,
Some(s) => s == value,
}
}
fn matches_hash(matcher: &Option<String>, hash: &TaggedHash) -> bool {
match matcher.as_deref() {
None | Some("*") => true,
Some(s) => s == hash.to_string(),
}
}
impl fmt::Display for Tri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Tri::True => "true",
Tri::False => "false",
Tri::Unknown => "?",
})
}
}
impl fmt::Display for Verdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Verdict::Pass => f.write_str("pass"),
Verdict::Unaudited => f.write_str("unaudited (no matching audit)"),
Verdict::Fail(reasons) => {
writeln!(f, "fail")?;
for r in reasons {
writeln!(f, " - {r}")?;
}
Ok(())
}
}
}
}
impl fmt::Display for FailureReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
FailureKind::NotAsserted => {
write!(f, "no audit asserted requirement {:?}", self.requirement)
}
FailureKind::Contradicted(c) => {
writeln!(f, "requirement {:?} contradicted by:", self.requirement)?;
for ac in c {
write!(f, " log {:?}:", ac.log)?;
for (name, tri) in &ac.relevant_claims {
write!(f, " {name}={tri}")?;
}
writeln!(f)?;
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::parse_str;
use openvet_crypto::TaggedHash;
use openvet_proto::Subject;
use std::collections::BTreeMap;
fn subj(reg: &str, pkg: &str, ver: &str) -> Subject {
Subject {
registry: reg.into(),
package: pkg.into(),
version: ver.into(),
variant: None,
hash: TaggedHash::tagged("sha256", [0; 32]),
}
}
fn audit_with(claims: &[(&str, bool)]) -> Audit {
Audit::builder()
.subject(subj("cargo", "anything", "0.0.0"))
.claims(
claims
.iter()
.map(|(k, v)| ((*k).to_string(), *v))
.collect::<BTreeMap<_, _>>(),
)
.build()
}
#[test]
fn passes_when_default_requirement_satisfied() {
let p = parse_str(
r#"
[requirement]
std-deploy = "safe-to-deploy"
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-deploy", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn unaudited_when_audit_set_is_empty() {
let p = parse_str(
r#"
[requirement]
std-deploy = "safe-to-deploy"
"#,
)
.unwrap();
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[]);
assert!(matches!(v, Verdict::Unaudited));
}
#[test]
fn empty_requirement_set_passes_trivially_even_when_unaudited() {
let p = parse_str(
r#"
[requirement]
r1 = "safe-to-deploy"
[[override]]
package = "x"
requirements = []
"#,
)
.unwrap();
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn fails_not_asserted_when_no_audit_speaks() {
let p = parse_str(
r#"
[requirement]
std-deploy = "safe-to-deploy"
"#,
)
.unwrap();
let a = audit_with(&[]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
match v {
Verdict::Fail(rs) => {
assert_eq!(rs.len(), 1);
assert!(matches!(rs[0].kind, FailureKind::NotAsserted));
}
_ => panic!("expected Fail"),
}
}
#[test]
fn fails_contradicted_when_audit_says_false() {
let p = parse_str(
r#"
[requirement]
std-deploy = "safe-to-deploy"
"#,
)
.unwrap();
let asserted = audit_with(&[("safe-to-deploy", true)]);
let denied = audit_with(&[("safe-to-deploy", false)]);
let v = evaluate(
&p,
&subj("cargo", "x", "1.0"),
&[("alice", &asserted), ("bob", &denied)],
);
match v {
Verdict::Fail(rs) => {
assert!(matches!(rs[0].kind, FailureKind::Contradicted(_)));
}
_ => panic!("expected Fail (one audit asserts; another contradicts)"),
}
}
#[test]
fn at_least_one_true_passes_when_others_unknown() {
let p = parse_str(
r#"
[requirement]
std-deploy = "safe-to-deploy"
"#,
)
.unwrap();
let asserted = audit_with(&[("safe-to-deploy", true)]);
let silent = audit_with(&[]);
let v = evaluate(
&p,
&subj("cargo", "x", "1.0"),
&[("alice", &asserted), ("bob", &silent)],
);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn override_replace_swaps_requirement_set() {
let p = parse_str(
r#"
[requirement]
r1 = "safe-to-deploy"
r2 = { condition = "safe-to-run", default = false }
[[override]]
package = "x"
requirements = ["r2"]
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-run", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn override_patch_adds_and_removes() {
let p = parse_str(
r#"
[requirement]
r1 = "safe-to-deploy"
r2 = { condition = "safe-to-run", default = false }
[[override]]
package = "x"
requirements = { add = ["r2"], remove = ["r1"] }
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-run", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn override_only_applies_to_matching_subject() {
let p = parse_str(
r#"
[requirement]
r1 = "safe-to-deploy"
r2 = { condition = "safe-to-run", default = false }
[[override]]
package = "x"
requirements = ["r2"]
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-run", true)]);
let v = evaluate(&p, &subj("cargo", "y", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Fail(_)));
}
#[test]
fn alias_translates_claim_name_per_log() {
let p = parse_str(
r#"
[requirement]
r = "safe-to-run"
[alias]
safe-to-run = ["mozilla:runtime-safe"]
"#,
)
.unwrap();
let m = audit_with(&[("runtime-safe", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("mozilla", &m)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn alias_falls_back_to_canonical_for_unlisted_log() {
let p = parse_str(
r#"
[requirement]
r = "safe-to-run"
[alias]
safe-to-run = ["mozilla:runtime-safe"]
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-run", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn all_requirements_must_pass() {
let p = parse_str(
r#"
[requirement]
r1 = "safe-to-deploy"
r2 = "safe-to-run"
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-deploy", true)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Fail(_)));
}
#[test]
fn version_matcher_requires_exact() {
let p = parse_str(
r#"
[requirement]
r = "safe-to-deploy"
[[override]]
package = "x"
version = "1.0.0"
requirements = []
"#,
)
.unwrap();
let a = audit_with(&[]);
let v = evaluate(&p, &subj("cargo", "x", "1.0.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
let v = evaluate(&p, &subj("cargo", "x", "1.0.1"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Fail(_)));
}
#[test]
fn star_is_explicit_wildcard() {
let p = parse_str(
r#"
[requirement]
r = "safe-to-deploy"
[[override]]
registry = "*"
package = "x"
requirements = []
"#,
)
.unwrap();
let a = audit_with(&[]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
assert!(matches!(v, Verdict::Pass));
}
#[test]
fn display_prose_for_fail_includes_diagnostic() {
let p = parse_str(
r#"
[requirement]
r = "safe-to-deploy and safe-to-run"
"#,
)
.unwrap();
let a = audit_with(&[("safe-to-deploy", true), ("safe-to-run", false)]);
let v = evaluate(&p, &subj("cargo", "x", "1.0"), &[("alice", &a)]);
let s = format!("{v}");
assert!(s.contains("fail"));
assert!(s.contains("safe-to-run"));
assert!(s.contains("alice"));
}
}