use crate::EvaluationError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
pub enabled: bool,
pub log_directory: PathBuf,
pub max_file_size: usize,
pub rotation_policy: RotationPolicy,
pub retention_days: u32,
pub log_sensitive_data: bool,
pub encryption: Option<EncryptionConfig>,
pub tamper_protection: bool,
pub realtime_monitoring: bool,
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
enabled: true,
log_directory: PathBuf::from("./audit_logs"),
max_file_size: 100 * 1024 * 1024, rotation_policy: RotationPolicy::Daily,
retention_days: 90,
log_sensitive_data: false,
encryption: None,
tamper_protection: true,
realtime_monitoring: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RotationPolicy {
Daily,
Weekly,
Monthly,
SizeBased,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionConfig {
pub algorithm: String,
pub key_id: String,
pub compress: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub event_id: Uuid,
pub timestamp: SystemTime,
pub timestamp_iso: String,
pub event_type: AuditEventType,
pub severity: SeverityLevel,
pub actor: Actor,
pub resource: Resource,
pub action: String,
pub result: ActionResult,
pub client_info: ClientInfo,
pub context: HashMap<String, serde_json::Value>,
pub duration_ms: Option<u64>,
pub integrity_hash: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditEventType {
Authentication,
Authorization,
DataAccess,
DataModification,
ConfigurationChange,
EvaluationExecution,
SystemAdmin,
Security,
Compliance,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SeverityLevel {
Debug,
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Actor {
pub id: String,
pub actor_type: ActorType,
pub name: Option<String>,
pub ip_address: Option<String>,
pub session_id: Option<String>,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActorType {
User,
Service,
System,
ApiClient,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub id: String,
pub resource_type: ResourceType,
pub name: Option<String>,
pub parent: Option<String>,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResourceType {
AudioFile,
EvaluationResult,
Model,
Dataset,
Configuration,
UserAccount,
ApiKey,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionResult {
pub success: bool,
pub status_code: u16,
pub message: Option<String>,
pub error_details: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub user_agent: Option<String>,
pub application: Option<String>,
pub version: Option<String>,
pub location: Option<String>,
}
pub struct AuditTrail {
config: AuditConfig,
current_file: Arc<Mutex<Option<BufWriter<File>>>>,
current_file_path: Arc<Mutex<Option<PathBuf>>>,
current_file_size: Arc<Mutex<usize>>,
event_buffer: Arc<Mutex<Vec<AuditEvent>>>,
stats: Arc<Mutex<AuditStatistics>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditStatistics {
pub total_events: u64,
pub events_by_type: HashMap<String, u64>,
pub events_by_severity: HashMap<String, u64>,
pub failed_writes: u64,
pub buffer_overflows: u64,
pub last_event_time: Option<SystemTime>,
}
impl AuditTrail {
pub fn new(config: AuditConfig) -> Result<Self, EvaluationError> {
if config.enabled {
std::fs::create_dir_all(&config.log_directory).map_err(|e| {
EvaluationError::ProcessingError {
message: format!("Failed to create audit log directory: {}", e),
source: None,
}
})?;
}
Ok(Self {
config,
current_file: Arc::new(Mutex::new(None)),
current_file_path: Arc::new(Mutex::new(None)),
current_file_size: Arc::new(Mutex::new(0)),
event_buffer: Arc::new(Mutex::new(Vec::new())),
stats: Arc::new(Mutex::new(AuditStatistics::default())),
})
}
pub fn log_event(&self, mut event: AuditEvent) -> Result<(), EvaluationError> {
if !self.config.enabled {
return Ok(());
}
if self.config.tamper_protection {
event.integrity_hash = Some(self.compute_integrity_hash(&event));
}
self.update_statistics(&event);
let event_json =
serde_json::to_string(&event).map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to serialize audit event: {}", e),
source: None,
})?;
self.write_event(&event_json)?;
Ok(())
}
pub fn log_evaluation(
&self,
actor: Actor,
resource: Resource,
result: ActionResult,
duration_ms: u64,
context: HashMap<String, serde_json::Value>,
) -> Result<(), EvaluationError> {
let event = AuditEvent {
event_id: Uuid::new_v4(),
timestamp: SystemTime::now(),
timestamp_iso: Utc::now().to_rfc3339(),
event_type: AuditEventType::EvaluationExecution,
severity: if result.success {
SeverityLevel::Info
} else {
SeverityLevel::Error
},
actor,
resource,
action: "evaluate".to_string(),
result,
client_info: ClientInfo {
user_agent: None,
application: Some("voirs-evaluation".to_string()),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
location: None,
},
context,
duration_ms: Some(duration_ms),
integrity_hash: None,
};
self.log_event(event)
}
pub fn log_authentication(
&self,
actor: Actor,
success: bool,
method: &str,
) -> Result<(), EvaluationError> {
let event = AuditEvent {
event_id: Uuid::new_v4(),
timestamp: SystemTime::now(),
timestamp_iso: Utc::now().to_rfc3339(),
event_type: AuditEventType::Authentication,
severity: if success {
SeverityLevel::Info
} else {
SeverityLevel::Warning
},
actor,
resource: Resource {
id: "authentication".to_string(),
resource_type: ResourceType::System,
name: Some("Authentication System".to_string()),
parent: None,
attributes: HashMap::new(),
},
action: format!("authenticate_{}", method),
result: ActionResult {
success,
status_code: if success { 200 } else { 401 },
message: Some(
if success {
"Authentication successful"
} else {
"Authentication failed"
}
.to_string(),
),
error_details: None,
},
client_info: ClientInfo {
user_agent: None,
application: None,
version: None,
location: None,
},
context: HashMap::from([(
"auth_method".to_string(),
serde_json::Value::String(method.to_string()),
)]),
duration_ms: None,
integrity_hash: None,
};
self.log_event(event)
}
pub fn log_data_access(
&self,
actor: Actor,
resource: Resource,
action: &str,
success: bool,
) -> Result<(), EvaluationError> {
let event = AuditEvent {
event_id: Uuid::new_v4(),
timestamp: SystemTime::now(),
timestamp_iso: Utc::now().to_rfc3339(),
event_type: AuditEventType::DataAccess,
severity: SeverityLevel::Info,
actor,
resource,
action: action.to_string(),
result: ActionResult {
success,
status_code: if success { 200 } else { 403 },
message: None,
error_details: None,
},
client_info: ClientInfo {
user_agent: None,
application: None,
version: None,
location: None,
},
context: HashMap::new(),
duration_ms: None,
integrity_hash: None,
};
self.log_event(event)
}
pub fn log_security_event(
&self,
actor: Actor,
event_description: &str,
severity: SeverityLevel,
details: HashMap<String, serde_json::Value>,
) -> Result<(), EvaluationError> {
let event = AuditEvent {
event_id: Uuid::new_v4(),
timestamp: SystemTime::now(),
timestamp_iso: Utc::now().to_rfc3339(),
event_type: AuditEventType::Security,
severity,
actor,
resource: Resource {
id: "security".to_string(),
resource_type: ResourceType::System,
name: Some("Security System".to_string()),
parent: None,
attributes: HashMap::new(),
},
action: event_description.to_string(),
result: ActionResult {
success: true,
status_code: 200,
message: Some(event_description.to_string()),
error_details: None,
},
client_info: ClientInfo {
user_agent: None,
application: None,
version: None,
location: None,
},
context: details,
duration_ms: None,
integrity_hash: None,
};
self.log_event(event)
}
fn write_event(&self, event_json: &str) -> Result<(), EvaluationError> {
let mut file_guard = self
.current_file
.lock()
.expect("lock should not be poisoned");
let mut path_guard = self
.current_file_path
.lock()
.expect("lock should not be poisoned");
let mut size_guard = self
.current_file_size
.lock()
.expect("lock should not be poisoned");
let needs_rotation = match self.config.rotation_policy {
RotationPolicy::SizeBased => *size_guard >= self.config.max_file_size,
_ => file_guard.is_none(), };
if needs_rotation || file_guard.is_none() {
self.rotate_log_file(&mut file_guard, &mut path_guard, &mut size_guard)?;
}
if let Some(ref mut writer) = *file_guard {
writeln!(writer, "{}", event_json).map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to write audit event: {}", e),
source: None,
})?;
*size_guard += event_json.len() + 1;
writer
.flush()
.map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to flush audit log: {}", e),
source: None,
})?;
}
Ok(())
}
fn rotate_log_file(
&self,
file_guard: &mut Option<BufWriter<File>>,
path_guard: &mut Option<PathBuf>,
size_guard: &mut usize,
) -> Result<(), EvaluationError> {
if let Some(mut writer) = file_guard.take() {
writer.flush().ok();
}
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!("audit_{}.jsonl", timestamp);
let filepath = self.config.log_directory.join(filename);
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&filepath)
.map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to open audit log file: {}", e),
source: None,
})?;
*file_guard = Some(BufWriter::new(file));
*path_guard = Some(filepath);
*size_guard = 0;
Ok(())
}
fn compute_integrity_hash(&self, event: &AuditEvent) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
event.event_id.hash(&mut hasher);
event.action.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
fn update_statistics(&self, event: &AuditEvent) {
if let Ok(mut stats) = self.stats.lock() {
stats.total_events += 1;
let event_type_key = format!("{:?}", event.event_type);
*stats.events_by_type.entry(event_type_key).or_insert(0) += 1;
let severity_key = format!("{:?}", event.severity);
*stats.events_by_severity.entry(severity_key).or_insert(0) += 1;
stats.last_event_time = Some(event.timestamp);
}
}
pub fn get_statistics(&self) -> AuditStatistics {
self.stats
.lock()
.expect("lock should not be poisoned")
.clone()
}
pub fn query_events(&self, query: AuditQuery) -> Result<Vec<AuditEvent>, EvaluationError> {
let mut results = Vec::new();
let entries = std::fs::read_dir(&self.config.log_directory).map_err(|e| {
EvaluationError::ProcessingError {
message: format!("Failed to read audit log directory: {}", e),
source: None,
}
})?;
for entry in entries.flatten() {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
if let Some(filename) = entry.file_name().to_str() {
if filename.starts_with("audit_") && filename.ends_with(".jsonl") {
if let Ok(events) = self.read_events_from_file(&entry.path()) {
for event in events {
if query.matches(&event) {
results.push(event);
}
}
}
}
}
}
}
}
results.sort_by_key(|b| std::cmp::Reverse(b.timestamp));
if let Some(limit) = query.limit {
results.truncate(limit);
}
Ok(results)
}
fn read_events_from_file(&self, path: &Path) -> Result<Vec<AuditEvent>, EvaluationError> {
use std::io::{BufRead, BufReader};
let file = File::open(path).map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to open audit log file: {}", e),
source: None,
})?;
let reader = BufReader::new(file);
let mut events = Vec::new();
for line_result in reader.lines() {
if let Ok(line_content) = line_result {
if let Ok(event) = serde_json::from_str::<AuditEvent>(&line_content) {
events.push(event);
}
}
}
Ok(events)
}
pub fn flush(&self) -> Result<(), EvaluationError> {
if let Ok(mut file_guard) = self.current_file.lock() {
if let Some(ref mut writer) = *file_guard {
writer
.flush()
.map_err(|e| EvaluationError::ProcessingError {
message: format!("Failed to flush audit log: {}", e),
source: None,
})?;
}
}
Ok(())
}
pub fn generate_compliance_report(
&self,
start_time: SystemTime,
end_time: SystemTime,
) -> Result<ComplianceReport, EvaluationError> {
let query = AuditQuery {
start_time: Some(start_time),
end_time: Some(end_time),
event_types: None,
severity_levels: None,
actors: None,
limit: None,
};
let events = self.query_events(query)?;
let total_events = events.len();
let security_events = events
.iter()
.filter(|e| e.event_type == AuditEventType::Security)
.count();
let failed_authentications = events
.iter()
.filter(|e| e.event_type == AuditEventType::Authentication && !e.result.success)
.count();
let data_access_events = events
.iter()
.filter(|e| e.event_type == AuditEventType::DataAccess)
.count();
Ok(ComplianceReport {
start_time,
end_time,
total_events,
security_events,
failed_authentications,
data_access_events,
events_by_type: self.count_events_by_type(&events),
events_by_severity: self.count_events_by_severity(&events),
top_actors: self.get_top_actors(&events, 10),
anomalies: self.detect_anomalies(&events),
})
}
fn count_events_by_type(&self, events: &[AuditEvent]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for event in events {
let key = format!("{:?}", event.event_type);
*counts.entry(key).or_insert(0) += 1;
}
counts
}
fn count_events_by_severity(&self, events: &[AuditEvent]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for event in events {
let key = format!("{:?}", event.severity);
*counts.entry(key).or_insert(0) += 1;
}
counts
}
fn get_top_actors(&self, events: &[AuditEvent], limit: usize) -> Vec<(String, usize)> {
let mut actor_counts: HashMap<String, usize> = HashMap::new();
for event in events {
*actor_counts.entry(event.actor.id.clone()).or_insert(0) += 1;
}
let mut sorted: Vec<_> = actor_counts.into_iter().collect();
sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
sorted.truncate(limit);
sorted
}
fn detect_anomalies(&self, events: &[AuditEvent]) -> Vec<String> {
let mut anomalies = Vec::new();
let failed_auth_count = events
.iter()
.filter(|e| e.event_type == AuditEventType::Authentication && !e.result.success)
.count();
if failed_auth_count > 5 {
anomalies.push(format!(
"High number of failed authentication attempts: {}",
failed_auth_count
));
}
let critical_count = events
.iter()
.filter(|e| e.severity == SeverityLevel::Critical)
.count();
if critical_count > 0 {
anomalies.push(format!(
"Critical security events detected: {}",
critical_count
));
}
anomalies
}
}
impl Drop for AuditTrail {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[derive(Debug, Clone, Default)]
pub struct AuditQuery {
pub start_time: Option<SystemTime>,
pub end_time: Option<SystemTime>,
pub event_types: Option<Vec<AuditEventType>>,
pub severity_levels: Option<Vec<SeverityLevel>>,
pub actors: Option<Vec<String>>,
pub limit: Option<usize>,
}
impl AuditQuery {
fn matches(&self, event: &AuditEvent) -> bool {
if let Some(start) = self.start_time {
if event.timestamp < start {
return false;
}
}
if let Some(end) = self.end_time {
if event.timestamp > end {
return false;
}
}
if let Some(ref types) = self.event_types {
if !types.contains(&event.event_type) {
return false;
}
}
if let Some(ref severities) = self.severity_levels {
if !severities.contains(&event.severity) {
return false;
}
}
if let Some(ref actors) = self.actors {
if !actors.contains(&event.actor.id) {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceReport {
pub start_time: SystemTime,
pub end_time: SystemTime,
pub total_events: usize,
pub security_events: usize,
pub failed_authentications: usize,
pub data_access_events: usize,
pub events_by_type: HashMap<String, usize>,
pub events_by_severity: HashMap<String, usize>,
pub top_actors: Vec<(String, usize)>,
pub anomalies: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_audit_trail_creation() {
let temp_dir = env::temp_dir().join("voirs_audit_test");
let config = AuditConfig {
log_directory: temp_dir,
..Default::default()
};
let trail = AuditTrail::new(config);
assert!(trail.is_ok());
}
#[test]
fn test_log_evaluation_event() {
let temp_dir = env::temp_dir().join("voirs_audit_test_eval");
let config = AuditConfig {
log_directory: temp_dir.clone(),
..Default::default()
};
let trail = AuditTrail::new(config).unwrap();
let actor = Actor {
id: "user123".to_string(),
actor_type: ActorType::User,
name: Some("Test User".to_string()),
ip_address: Some("127.0.0.1".to_string()),
session_id: Some("session123".to_string()),
attributes: HashMap::new(),
};
let resource = Resource {
id: "audio123".to_string(),
resource_type: ResourceType::AudioFile,
name: Some("test.wav".to_string()),
parent: None,
attributes: HashMap::new(),
};
let result = ActionResult {
success: true,
status_code: 200,
message: Some("Evaluation completed".to_string()),
error_details: None,
};
let log_result = trail.log_evaluation(actor, resource, result, 150, HashMap::new());
assert!(log_result.is_ok());
let stats = trail.get_statistics();
assert_eq!(stats.total_events, 1);
std::fs::remove_dir_all(temp_dir).ok();
}
#[test]
fn test_log_authentication_event() {
let temp_dir = env::temp_dir().join("voirs_audit_test_auth");
let config = AuditConfig {
log_directory: temp_dir.clone(),
..Default::default()
};
let trail = AuditTrail::new(config).unwrap();
let actor = Actor {
id: "user456".to_string(),
actor_type: ActorType::User,
name: Some("Test User".to_string()),
ip_address: Some("192.168.1.1".to_string()),
session_id: None,
attributes: HashMap::new(),
};
let result = trail.log_authentication(actor, true, "api_key");
assert!(result.is_ok());
std::fs::remove_dir_all(temp_dir).ok();
}
#[test]
fn test_audit_statistics() {
let temp_dir = env::temp_dir().join("voirs_audit_test_stats");
let config = AuditConfig {
log_directory: temp_dir.clone(),
..Default::default()
};
let trail = AuditTrail::new(config).unwrap();
for i in 0..5 {
let actor = Actor {
id: format!("user{}", i),
actor_type: ActorType::User,
name: None,
ip_address: None,
session_id: None,
attributes: HashMap::new(),
};
trail.log_authentication(actor, i % 2 == 0, "password").ok();
}
let stats = trail.get_statistics();
assert_eq!(stats.total_events, 5);
assert!(stats.events_by_type.contains_key("Authentication"));
std::fs::remove_dir_all(temp_dir).ok();
}
#[test]
fn test_compliance_report() {
let temp_dir = env::temp_dir().join("voirs_audit_test_compliance");
let config = AuditConfig {
log_directory: temp_dir.clone(),
..Default::default()
};
let trail = AuditTrail::new(config).unwrap();
let actor = Actor {
id: "user789".to_string(),
actor_type: ActorType::User,
name: None,
ip_address: None,
session_id: None,
attributes: HashMap::new(),
};
trail
.log_authentication(actor.clone(), true, "api_key")
.ok();
trail.log_authentication(actor, false, "password").ok();
let start_time = SystemTime::now() - Duration::from_secs(3600);
let end_time = SystemTime::now();
let report = trail.generate_compliance_report(start_time, end_time);
assert!(report.is_ok());
let report = report.unwrap();
assert!(report.total_events > 0);
std::fs::remove_dir_all(temp_dir).ok();
}
}