use std::collections::{HashMap, HashSet};
use std::path::Path;
use ito_domain::audit::event::AuditEvent;
use super::reader::{EventFilter, read_audit_events, read_audit_events_filtered};
#[derive(Debug, Clone)]
pub struct AuditIssue {
pub level: String,
pub message: String,
pub event_index: usize,
}
#[derive(Debug)]
pub struct AuditValidationReport {
pub event_count: usize,
pub issues: Vec<AuditIssue>,
pub valid: bool,
}
pub fn validate_audit_log(ito_path: &Path, change_id: Option<&str>) -> AuditValidationReport {
let events = if let Some(change_id) = change_id {
let filter = EventFilter {
scope: Some(change_id.to_string()),
..Default::default()
};
read_audit_events_filtered(ito_path, &filter)
} else {
read_audit_events(ito_path)
};
let issues = run_checks(&events);
let has_errors = issues.iter().any(|i| i.level == "error");
AuditValidationReport {
event_count: events.len(),
valid: !has_errors,
issues,
}
}
fn run_checks(events: &[AuditEvent]) -> Vec<AuditIssue> {
let mut issues = Vec::new();
issues.extend(check_duplicate_creates(events));
issues.extend(check_invalid_status_transitions(events));
issues.extend(check_timestamp_ordering(events));
issues.sort_by_key(|i| i.event_index);
issues
}
fn check_duplicate_creates(events: &[AuditEvent]) -> Vec<AuditIssue> {
let mut issues = Vec::new();
let mut seen: HashSet<(String, String, Option<String>)> = HashSet::new();
for (i, event) in events.iter().enumerate() {
if event.op != "create" && event.op != "add" {
continue;
}
let key = (
event.entity.clone(),
event.entity_id.clone(),
event.scope.clone(),
);
if !seen.insert(key.clone()) {
issues.push(AuditIssue {
level: "warning".to_string(),
message: format!(
"Duplicate {} event for {}/{} (scope: {:?})",
event.op, event.entity, event.entity_id, event.scope
),
event_index: i,
});
}
}
issues
}
fn check_invalid_status_transitions(events: &[AuditEvent]) -> Vec<AuditIssue> {
let mut issues = Vec::new();
let mut last_status: HashMap<(String, String, Option<String>), String> = HashMap::new();
for (i, event) in events.iter().enumerate() {
if event.op != "status_change" {
if let Some(to) = &event.to {
let key = (
event.entity.clone(),
event.entity_id.clone(),
event.scope.clone(),
);
last_status.insert(key, to.clone());
}
continue;
}
let key = (
event.entity.clone(),
event.entity_id.clone(),
event.scope.clone(),
);
if let Some(from) = &event.from
&& let Some(last) = last_status.get(&key)
&& last != from
{
issues.push(AuditIssue {
level: "warning".to_string(),
message: format!(
"Status transition mismatch for {}/{}: expected from='{}' but event says from='{}'",
event.entity, event.entity_id, last, from
),
event_index: i,
});
}
if let Some(to) = &event.to {
last_status.insert(key, to.clone());
}
}
issues
}
fn check_timestamp_ordering(events: &[AuditEvent]) -> Vec<AuditIssue> {
let mut issues = Vec::new();
for i in 1..events.len() {
if events[i].ts < events[i - 1].ts {
issues.push(AuditIssue {
level: "warning".to_string(),
message: format!(
"Timestamp ordering violation: event {} ({}) is earlier than event {} ({})",
i + 1,
events[i].ts,
i,
events[i - 1].ts
),
event_index: i,
});
}
}
issues
}
#[cfg(test)]
#[path = "validate_tests.rs"]
mod validate_tests;