ito_core/audit/
validate.rs1use std::collections::{HashMap, HashSet};
9use std::path::Path;
10
11use ito_domain::audit::event::AuditEvent;
12
13use super::reader::{EventFilter, read_audit_events, read_audit_events_filtered};
14
15#[derive(Debug, Clone)]
17pub struct AuditIssue {
18 pub level: String,
20 pub message: String,
22 pub event_index: usize,
24}
25
26#[derive(Debug)]
28pub struct AuditValidationReport {
29 pub event_count: usize,
31 pub issues: Vec<AuditIssue>,
33 pub valid: bool,
35}
36
37pub fn validate_audit_log(ito_path: &Path, change_id: Option<&str>) -> AuditValidationReport {
39 let events = if let Some(change_id) = change_id {
40 let filter = EventFilter {
41 scope: Some(change_id.to_string()),
42 ..Default::default()
43 };
44 read_audit_events_filtered(ito_path, &filter)
45 } else {
46 read_audit_events(ito_path)
47 };
48
49 let issues = run_checks(&events);
50
51 let has_errors = issues.iter().any(|i| i.level == "error");
52
53 AuditValidationReport {
54 event_count: events.len(),
55 valid: !has_errors,
56 issues,
57 }
58}
59
60fn run_checks(events: &[AuditEvent]) -> Vec<AuditIssue> {
62 let mut issues = Vec::new();
63
64 issues.extend(check_duplicate_creates(events));
65 issues.extend(check_invalid_status_transitions(events));
66 issues.extend(check_timestamp_ordering(events));
67
68 issues.sort_by_key(|i| i.event_index);
70
71 issues
72}
73
74fn check_duplicate_creates(events: &[AuditEvent]) -> Vec<AuditIssue> {
76 let mut issues = Vec::new();
77 let mut seen: HashSet<(String, String, Option<String>)> = HashSet::new();
78
79 for (i, event) in events.iter().enumerate() {
80 if event.op != "create" && event.op != "add" {
81 continue;
82 }
83 let key = (
84 event.entity.clone(),
85 event.entity_id.clone(),
86 event.scope.clone(),
87 );
88 if !seen.insert(key.clone()) {
89 issues.push(AuditIssue {
90 level: "warning".to_string(),
91 message: format!(
92 "Duplicate {} event for {}/{} (scope: {:?})",
93 event.op, event.entity, event.entity_id, event.scope
94 ),
95 event_index: i,
96 });
97 }
98 }
99
100 issues
101}
102
103fn check_invalid_status_transitions(events: &[AuditEvent]) -> Vec<AuditIssue> {
105 let mut issues = Vec::new();
106
107 let mut last_status: HashMap<(String, String, Option<String>), String> = HashMap::new();
109
110 for (i, event) in events.iter().enumerate() {
111 if event.op != "status_change" {
112 if let Some(to) = &event.to {
114 let key = (
115 event.entity.clone(),
116 event.entity_id.clone(),
117 event.scope.clone(),
118 );
119 last_status.insert(key, to.clone());
120 }
121 continue;
122 }
123
124 let key = (
125 event.entity.clone(),
126 event.entity_id.clone(),
127 event.scope.clone(),
128 );
129
130 if let Some(from) = &event.from
132 && let Some(last) = last_status.get(&key)
133 && last != from
134 {
135 issues.push(AuditIssue {
136 level: "warning".to_string(),
137 message: format!(
138 "Status transition mismatch for {}/{}: expected from='{}' but event says from='{}'",
139 event.entity, event.entity_id, last, from
140 ),
141 event_index: i,
142 });
143 }
144
145 if let Some(to) = &event.to {
147 last_status.insert(key, to.clone());
148 }
149 }
150
151 issues
152}
153
154fn check_timestamp_ordering(events: &[AuditEvent]) -> Vec<AuditIssue> {
156 let mut issues = Vec::new();
157
158 for i in 1..events.len() {
159 if events[i].ts < events[i - 1].ts {
160 issues.push(AuditIssue {
161 level: "warning".to_string(),
162 message: format!(
163 "Timestamp ordering violation: event {} ({}) is earlier than event {} ({})",
164 i + 1,
165 events[i].ts,
166 i,
167 events[i - 1].ts
168 ),
169 event_index: i,
170 });
171 }
172 }
173
174 issues
175}
176
177#[cfg(test)]
178#[path = "validate_tests.rs"]
179mod validate_tests;