aaai_core/audit/
engine.rs1use crate::audit::warning;
4use crate::config::definition::AuditDefinition;
5use crate::diff::entry::{DiffEntry, DiffType};
6use super::result::{AuditResult, AuditStatus, FileAuditResult};
7use super::strategy;
8
9pub struct AuditEngine;
26
27#[derive(Debug, Clone, Default)]
29pub struct AuditOptions {
30 pub suppress_warnings: Vec<String>,
32}
33
34impl AuditEngine {
35 pub fn evaluate(diffs: &[DiffEntry], definition: &AuditDefinition) -> AuditResult {
40 Self::evaluate_with_options(diffs, definition, &AuditOptions::default())
41 }
42
43 pub fn evaluate_with_options(
48 diffs: &[DiffEntry],
49 definition: &AuditDefinition,
50 options: &AuditOptions,
51 ) -> AuditResult {
52 let mut results = Vec::new();
53
54 for diff in diffs {
55 let mut result = judge(diff, definition);
56 if !options.suppress_warnings.is_empty() {
58 result.warnings.retain(|w| {
59 !options.suppress_warnings.iter().any(|s| s == w.kind())
60 });
61 }
62 results.push(result);
63 }
64
65 AuditResult::new(results)
66 }
67}
68
69fn judge(diff: &DiffEntry, definition: &AuditDefinition) -> FileAuditResult {
70 if diff.diff_type.is_error() {
72 return FileAuditResult {
73 diff: diff.clone(),
74 entry: None,
75 status: AuditStatus::Error,
76 detail: diff.error_detail.clone().or_else(|| {
77 Some("File could not be read or compared.".into())
78 }),
79 warnings: Vec::new(),
80 };
81 }
82
83 if diff.diff_type == DiffType::Unchanged {
85 return FileAuditResult {
86 diff: diff.clone(),
87 entry: definition.find_entry(&diff.path).cloned(),
88 status: AuditStatus::Ok,
89 detail: None,
90 warnings: Vec::new(),
91 };
92 }
93
94 let entry = match definition.find_entry(&diff.path) {
96 Some(e) => e,
97 None => {
98 return FileAuditResult {
99 diff: diff.clone(),
100 entry: None,
101 status: AuditStatus::Pending,
102 detail: Some("No audit rule defined for this path.".into()),
103 warnings: Vec::new(),
104 };
105 }
106 };
107
108 if !entry.enabled {
110 return FileAuditResult {
111 diff: diff.clone(),
112 entry: Some(entry.clone()),
113 status: AuditStatus::Ignored,
114 detail: Some("Entry is disabled.".into()),
115 warnings: Vec::new(),
116 };
117 }
118
119 if entry.is_expired() {
123 return FileAuditResult {
124 diff: diff.clone(),
125 entry: Some(entry.clone()),
126 status: AuditStatus::Pending,
127 detail: Some("Approval has expired and needs renewal.".into()),
128 warnings: Vec::new(),
129 };
130 }
131
132 if entry.reason.trim().is_empty() {
134 return FileAuditResult {
135 diff: diff.clone(),
136 entry: Some(entry.clone()),
137 status: AuditStatus::Pending,
138 detail: Some("Entry exists but has no reason — human approval required.".into()),
139 warnings: Vec::new(),
140 };
141 }
142
143 if entry.diff_type != diff.diff_type {
145 return FileAuditResult {
146 diff: diff.clone(),
147 entry: Some(entry.clone()),
148 status: AuditStatus::Failed,
149 detail: Some(format!(
150 "Expected diff type {:?} but found {:?}.",
151 entry.diff_type, diff.diff_type
152 )),
153 warnings: Vec::new(),
154 };
155 }
156
157 match strategy::evaluate(&entry.strategy, diff) {
159 Ok(()) => {
160 let warns = warning::collect(diff, entry);
161 FileAuditResult {
162 diff: diff.clone(),
163 entry: Some(entry.clone()),
164 status: AuditStatus::Ok,
165 detail: None,
166 warnings: warns,
167 }
168 }
169 Err(msg) => FileAuditResult {
170 diff: diff.clone(),
171 entry: Some(entry.clone()),
172 status: AuditStatus::Failed,
173 detail: Some(msg),
174 warnings: Vec::new(),
175 },
176 }
177}