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
46fn 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}