aaai_core/audit/
warning.rs1use crate::diff::entry::{DiffEntry, LARGE_FILE_THRESHOLD, fmt_size};
12use crate::config::definition::{AuditEntry, AuditStrategy};
13
14#[derive(Debug, Clone)]
16pub enum AuditWarning {
17 LargeFileStrategy {
19 path: String,
20 strategy: &'static str,
21 size_bytes: u64,
22 },
23 NoStrategyOnModified { path: String },
26 NoApprover { path: String },
28}
29
30impl AuditWarning {
31 pub fn message(&self) -> String {
33 match self {
34 AuditWarning::LargeFileStrategy { path, strategy, size_bytes } =>
35 format!(
36 "{path}: {strategy} strategy applied to a large file ({}). \
37 Consider using Checksum instead.",
38 fmt_size(*size_bytes)
39 ),
40 AuditWarning::NoStrategyOnModified { path } =>
41 format!("{path}: Modified file uses `None` strategy — content not verified."),
42 AuditWarning::NoApprover { path } =>
43 format!("{path}: Entry has no `approved_by` field — approver unknown."),
44 }
45 }
46
47 pub fn kind(&self) -> &'static str {
49 match self {
50 AuditWarning::LargeFileStrategy { .. } => "large-file",
51 AuditWarning::NoStrategyOnModified { .. } => "no-strategy",
52 AuditWarning::NoApprover { .. } => "no-approver",
53 }
54 }
55}
56
57pub fn collect(diff: &DiffEntry, entry: &AuditEntry) -> Vec<AuditWarning> {
61 let mut warns = Vec::new();
62
63 let size = diff.after_size.or(diff.before_size).unwrap_or(0);
65 if size > LARGE_FILE_THRESHOLD {
66 match &entry.strategy {
67 AuditStrategy::Exact { .. } | AuditStrategy::LineMatch { .. } => {
68 warns.push(AuditWarning::LargeFileStrategy {
69 path: diff.path.clone(),
70 strategy: entry.strategy.label(),
71 size_bytes: size,
72 });
73 }
74 _ => {}
75 }
76 }
77
78 if diff.diff_type == crate::diff::entry::DiffType::Modified {
80 if let AuditStrategy::None = entry.strategy {
81 warns.push(AuditWarning::NoStrategyOnModified { path: diff.path.clone() });
82 }
83 }
84
85 if entry.approved_by.is_none() && !entry.reason.trim().is_empty() {
87 warns.push(AuditWarning::NoApprover { path: diff.path.clone() });
88 }
89
90 warns
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::diff::entry::{DiffEntry, DiffType};
97 use crate::config::definition::{AuditEntry, AuditStrategy, LineRule, LineAction};
98
99 fn make_diff(path: &str, size: u64, diff_type: DiffType) -> DiffEntry {
100 DiffEntry {
101 path: path.to_string(), diff_type, is_dir: false,
102 before_text: None, after_text: None,
103 is_binary: false,
104 before_size: Some(size), after_size: Some(size),
105 before_sha256: None, after_sha256: None,
106 stats: None, error_detail: None,
107 }
108 }
109
110 fn make_entry(strategy: AuditStrategy) -> AuditEntry {
111 AuditEntry {
112 path: "f.txt".to_string(),
113 diff_type: DiffType::Modified,
114 reason: "test".to_string(),
115 strategy,
116 enabled: true,
117 ticket: None,
118 approved_by: None,
119 approved_at: None,
120 expires_at: None,
121 note: None,
122 created_at: None,
123 updated_at: None,
124 }
125 }
126
127 #[test]
128 fn large_file_with_linematch_warns() {
129 let diff = make_diff("big.txt", 2 * 1024 * 1024, DiffType::Modified);
130 let entry = make_entry(AuditStrategy::LineMatch {
131 rules: vec![LineRule { action: LineAction::Added, line: "x".into() }],
132 });
133 let warns = collect(&diff, &entry);
134 assert!(warns.iter().any(|w| matches!(w, AuditWarning::LargeFileStrategy { .. })),
135 "large LineMatch should warn");
136 }
137
138 #[test]
139 fn large_file_with_checksum_no_warn() {
140 let diff = make_diff("big.bin", 2 * 1024 * 1024, DiffType::Modified);
141 let entry = make_entry(AuditStrategy::Checksum { expected_sha256: "a".repeat(64) });
142 let warns = collect(&diff, &entry);
143 assert!(!warns.iter().any(|w| matches!(w, AuditWarning::LargeFileStrategy { .. })),
144 "Checksum on large file should not warn");
145 }
146
147 #[test]
148 fn none_strategy_on_modified_warns() {
149 let diff = make_diff("cfg.toml", 100, DiffType::Modified);
150 let entry = make_entry(AuditStrategy::None);
151 let warns = collect(&diff, &entry);
152 assert!(warns.iter().any(|w| matches!(w, AuditWarning::NoStrategyOnModified { .. })));
153 }
154
155 #[test]
156 fn none_strategy_on_added_no_warn() {
157 let diff = make_diff("new.txt", 100, DiffType::Added);
158 let mut entry = make_entry(AuditStrategy::None);
159 entry.diff_type = DiffType::Added;
160 let warns = collect(&diff, &entry);
161 assert!(!warns.iter().any(|w| matches!(w, AuditWarning::NoStrategyOnModified { .. })));
162 }
163
164 #[test]
165 fn no_approver_warns() {
166 let diff = make_diff("f.txt", 100, DiffType::Modified);
167 let entry = make_entry(AuditStrategy::None);
168 let warns = collect(&diff, &entry);
169 assert!(warns.iter().any(|w| matches!(w, AuditWarning::NoApprover { .. })));
170 }
171
172 #[test]
173 fn with_approver_no_warn() {
174 let diff = make_diff("f.txt", 100, DiffType::Modified);
175 let mut entry = make_entry(AuditStrategy::None);
176 entry.approved_by = Some("alice".into());
177 let warns = collect(&diff, &entry);
178 assert!(!warns.iter().any(|w| matches!(w, AuditWarning::NoApprover { .. })));
179 }
180}