use crate::core::config::FluxConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub policy: String,
pub message: String,
}
pub fn evaluate(config: &FluxConfig, approvals: u32) -> Vec<Violation> {
let has_tests = config.steps.iter().any(|s| s.name.contains("test"));
let has_security = config
.steps
.iter()
.any(|s| s.name.contains("security") || s.is_hook());
let mut violations = Vec::new();
for policy in &config.policies {
if policy.require_tests && !has_tests {
violations.push(Violation {
policy: policy.name.clone(),
message: "requires a test step, but the pipeline has none".into(),
});
}
if policy.require_security && !has_security {
violations.push(Violation {
policy: policy.name.clone(),
message: "requires a security step (a step named *security*, or a `tool` hook), but none is present"
.into(),
});
}
if approvals < policy.require_approvals {
violations.push(Violation {
policy: policy.name.clone(),
message: format!(
"requires {} approval(s), but {approvals} provided (set FLUX_APPROVALS)",
policy.require_approvals
),
});
}
}
violations
}
pub fn approvals_from_env() -> u32 {
std::env::var("FLUX_APPROVALS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::parse;
const POLICY: &str = "policy prod { require tests require security require approvals 2 }\n";
#[test]
fn passes_when_requirements_met() {
let src = format!(
"{POLICY}pipeline {{ step test {{ command \"cargo test\" }} step security {{ tool scanner }} }}"
);
let config = parse(&src).unwrap();
assert!(evaluate(&config, 2).is_empty());
}
#[test]
fn flags_missing_tests_security_and_approvals() {
let src = format!("{POLICY}pipeline {{ step build {{ command \"cargo build\" }} }}");
let config = parse(&src).unwrap();
let violations = evaluate(&config, 0);
assert_eq!(violations.len(), 3);
}
}