Skip to main content

cac_validator/
lib.rs

1use cac_core::violation::{ScanReport, ValidationReport, Violation};
2use cac_scanner::{ScanConfig, Scanner};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum ValidateError {
7    #[error("scan error: {0}")]
8    Scan(#[from] cac_scanner::ScanError),
9}
10
11pub struct Validator {
12    root: std::path::PathBuf,
13    policy_dir: std::path::PathBuf,
14}
15
16impl Validator {
17    pub fn new(root: impl Into<std::path::PathBuf>, policy_dir: impl Into<std::path::PathBuf>) -> Self {
18        Self {
19            root: root.into(),
20            policy_dir: policy_dir.into(),
21        }
22    }
23
24    pub fn validate_after_fix(
25        &self,
26        original: &ScanReport,
27        fixes_applied: usize,
28    ) -> Result<ValidationReport, ValidateError> {
29        let scanner = Scanner::from_config(ScanConfig::new(&self.root, &self.policy_dir))?;
30        let rescanned = scanner.scan()?;
31        let adversarial_notes = adversarial_review(&original.violations, &rescanned.violations);
32        let passed = rescanned.violations.is_empty();
33
34        Ok(ValidationReport {
35            validated_at: chrono::Utc::now().to_rfc3339(),
36            original_violations: original.violations.len(),
37            remaining_violations: rescanned.violations.len(),
38            fixes_applied,
39            passed,
40            adversarial_notes,
41            remaining: rescanned.violations,
42        })
43    }
44}
45
46/// CHP-inspired adversarial validation: challenge whether fixes merely hide violations.
47fn adversarial_review(before: &[Violation], after: &[Violation]) -> Vec<String> {
48    let mut notes = Vec::new();
49
50    if after.len() >= before.len() {
51        notes.push(
52            "Adversarial: fix pass did not reduce violation count — fixes may be cosmetic or incomplete."
53                .into(),
54        );
55    }
56
57    for v in after {
58        if v.rule_id.starts_with("secret-") && v.snippet.contains("env::var") {
59            notes.push(format!(
60                "Adversarial: {} still flagged — verify env var is not a placeholder.",
61                v.file_path
62            ));
63        }
64        if v.rule_id.starts_with("gdpr-") {
65            notes.push(format!(
66                "Adversarial: GDPR annotation missing near PII in {} — confirm lawful basis documented.",
67                v.file_path
68            ));
69        }
70    }
71
72    if notes.is_empty() {
73        notes.push(
74            "Adversarial: re-scan passed. Independent validator confirms no remaining policy violations."
75                .into(),
76        );
77    }
78
79    notes
80}