use crate::cryptographic_provenance::ProvenanceKeyPair;
use chrono::{DateTime, Utc};
use scirs2_core::metrics::{Counter, Timer};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use thiserror::Error;
use tracing::{debug, error, info, warn};
#[derive(Error, Debug)]
pub enum AuditError {
#[error("Failed to write audit log: {0}")]
WriteError(String),
#[error("Failed to read audit log: {0}")]
ReadError(String),
#[error("Audit log verification failed: {0}")]
VerificationFailed(String),
#[error("Log rotation failed: {0}")]
RotationFailed(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization error: {0}")]
SerializationError(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SecuritySeverity {
Info,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SecurityCategory {
Authentication,
Authorization,
DataAccess,
DataModification,
Configuration,
System,
Network,
Cryptographic,
PolicyViolation,
Anomaly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub severity: SecuritySeverity,
pub category: SecurityCategory,
pub event_type: String,
pub actor: Option<String>,
pub resource: Option<String>,
pub source_ip: Option<String>,
pub outcome: EventOutcome,
pub message: String,
pub metadata: HashMap<String, String>,
pub previous_hash: Option<String>,
pub signature: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EventOutcome {
Success,
Failure,
Partial,
Unknown,
}
impl SecurityEvent {
pub fn new(
severity: SecuritySeverity,
category: SecurityCategory,
event_type: String,
message: String,
) -> Self {
use scirs2_core::random::{rng, RngExt};
let mut rng_instance = rng();
let id = format!("evt_{:016x}", rng_instance.random::<u64>());
Self {
id,
timestamp: Utc::now(),
severity,
category,
event_type,
actor: None,
resource: None,
source_ip: None,
outcome: EventOutcome::Success,
message,
metadata: HashMap::new(),
previous_hash: None,
signature: None,
}
}
pub fn with_actor(mut self, actor: String) -> Self {
self.actor = Some(actor);
self
}
pub fn with_resource(mut self, resource: String) -> Self {
self.resource = Some(resource);
self
}
pub fn with_source_ip(mut self, ip: String) -> Self {
self.source_ip = Some(ip);
self
}
pub fn with_outcome(mut self, outcome: EventOutcome) -> Self {
self.outcome = outcome;
self
}
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
pub fn compute_hash(&self) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&self.id);
hasher.update(self.timestamp.to_rfc3339());
hasher.update(&self.event_type);
hasher.update(&self.message);
if let Some(ref actor) = self.actor {
hasher.update(actor);
}
if let Some(ref resource) = self.resource {
hasher.update(resource);
}
hex::encode(hasher.finalize())
}
}
#[derive(Debug, Clone)]
pub struct AuditConfig {
pub log_dir: PathBuf,
pub enable_signatures: bool,
pub max_file_size: u64,
pub max_rotations: usize,
pub min_severity: SecuritySeverity,
pub enable_anomaly_detection: bool,
pub buffer_size: usize,
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
log_dir: PathBuf::from("./audit_logs"),
enable_signatures: true,
max_file_size: 100 * 1024 * 1024, max_rotations: 10,
min_severity: SecuritySeverity::Info,
enable_anomaly_detection: true,
buffer_size: 1000,
}
}
}
pub struct SecurityAuditLogger {
config: AuditConfig,
current_file: Arc<Mutex<Option<BufWriter<File>>>>,
current_path: Arc<Mutex<PathBuf>>,
event_counter: Counter,
write_timer: Timer,
event_buffer: Arc<Mutex<VecDeque<SecurityEvent>>>,
last_hash: Arc<Mutex<Option<String>>>,
signing_key: Option<ProvenanceKeyPair>,
anomaly_detector: Arc<Mutex<AnomalyDetector>>,
}
impl SecurityAuditLogger {
pub fn new(config: AuditConfig) -> Result<Self, AuditError> {
std::fs::create_dir_all(&config.log_dir)?;
let event_counter = Counter::new("security_events_total".to_string());
let write_timer = Timer::new("audit_log_write_duration".to_string());
let signing_key = if config.enable_signatures {
Some(ProvenanceKeyPair::generate())
} else {
None
};
let logger = Self {
config: config.clone(),
current_file: Arc::new(Mutex::new(None)),
current_path: Arc::new(Mutex::new(PathBuf::new())),
event_counter,
write_timer,
event_buffer: Arc::new(Mutex::new(VecDeque::with_capacity(config.buffer_size))),
last_hash: Arc::new(Mutex::new(None)),
signing_key,
anomaly_detector: Arc::new(Mutex::new(AnomalyDetector::new())),
};
logger.rotate_log()?;
info!("Security audit logger initialized at {:?}", config.log_dir);
Ok(logger)
}
pub fn log_event(&self, mut event: SecurityEvent) -> Result<(), AuditError> {
if event.severity < self.config.min_severity {
return Ok(());
}
let mut last_hash = self.last_hash.lock().unwrap_or_else(|e| e.into_inner());
event.previous_hash = last_hash.clone();
if let Some(ref key_pair) = self.signing_key {
let event_hash = event.compute_hash();
let signature = key_pair.sign(event_hash.as_bytes());
event.signature = Some(hex::encode(signature.to_bytes()));
}
let current_hash = event.compute_hash();
*last_hash = Some(current_hash);
drop(last_hash);
let mut buffer = self.event_buffer.lock().unwrap_or_else(|e| e.into_inner());
if buffer.len() >= self.config.buffer_size {
buffer.pop_front();
}
buffer.push_back(event.clone());
drop(buffer);
if self.config.enable_anomaly_detection {
let mut detector = self
.anomaly_detector
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(anomaly) = detector.check_event(&event) {
warn!("Anomaly detected: {}", anomaly);
}
}
let _timer = self.write_timer.start();
self.write_event(&event)?;
self.event_counter.inc();
self.check_rotation()?;
Ok(())
}
fn write_event(&self, event: &SecurityEvent) -> Result<(), AuditError> {
let mut file_guard = self.current_file.lock().unwrap_or_else(|e| e.into_inner());
if let Some(ref mut writer) = *file_guard {
let json = serde_json::to_string(event)
.map_err(|e| AuditError::SerializationError(e.to_string()))?;
writeln!(writer, "{}", json)?;
writer.flush()?;
} else {
return Err(AuditError::WriteError("No active log file".to_string()));
}
Ok(())
}
fn rotate_log(&self) -> Result<(), AuditError> {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let log_path = self
.config
.log_dir
.join(format!("audit_{}.jsonl", timestamp));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
let mut current_file = self.current_file.lock().unwrap_or_else(|e| e.into_inner());
*current_file = Some(BufWriter::new(file));
let mut current_path = self.current_path.lock().unwrap_or_else(|e| e.into_inner());
*current_path = log_path.clone();
info!("Rotated audit log to {:?}", log_path);
self.cleanup_old_logs()?;
Ok(())
}
fn check_rotation(&self) -> Result<(), AuditError> {
let current_path = self.current_path.lock().unwrap_or_else(|e| e.into_inner());
if let Ok(metadata) = std::fs::metadata(&*current_path) {
if metadata.len() >= self.config.max_file_size {
drop(current_path);
self.rotate_log()?;
}
}
Ok(())
}
fn cleanup_old_logs(&self) -> Result<(), AuditError> {
let mut log_files: Vec<_> = std::fs::read_dir(&self.config.log_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.file_name().to_string_lossy().starts_with("audit_")
&& entry.file_name().to_string_lossy().ends_with(".jsonl")
})
.collect();
log_files.sort_by_key(|entry| entry.metadata().ok().and_then(|m| m.modified().ok()));
let to_remove = log_files.len().saturating_sub(self.config.max_rotations);
for entry in log_files.iter().take(to_remove) {
if let Err(e) = std::fs::remove_file(entry.path()) {
warn!("Failed to remove old log file {:?}: {}", entry.path(), e);
} else {
debug!("Removed old log file {:?}", entry.path());
}
}
Ok(())
}
pub fn query_recent_events(
&self,
limit: usize,
category: Option<SecurityCategory>,
min_severity: Option<SecuritySeverity>,
) -> Vec<SecurityEvent> {
let buffer = self.event_buffer.lock().unwrap_or_else(|e| e.into_inner());
buffer
.iter()
.rev()
.filter(|event| {
if let Some(ref cat) = category {
if &event.category != cat {
return false;
}
}
if let Some(ref sev) = min_severity {
if &event.severity < sev {
return false;
}
}
true
})
.take(limit)
.cloned()
.collect()
}
pub fn generate_report(&self, since: DateTime<Utc>) -> SecurityReport {
let buffer = self.event_buffer.lock().unwrap_or_else(|e| e.into_inner());
let events_in_period: Vec<_> = buffer
.iter()
.filter(|e| e.timestamp >= since)
.cloned()
.collect();
let total_events = events_in_period.len();
let mut by_severity = HashMap::new();
for event in &events_in_period {
*by_severity.entry(event.severity.clone()).or_insert(0) += 1;
}
let mut by_category = HashMap::new();
for event in &events_in_period {
*by_category.entry(event.category.clone()).or_insert(0) += 1;
}
let failures = events_in_period
.iter()
.filter(|e| matches!(e.outcome, EventOutcome::Failure))
.count();
let mut actor_counts: HashMap<String, usize> = HashMap::new();
for event in &events_in_period {
if let Some(ref actor) = event.actor {
*actor_counts.entry(actor.clone()).or_insert(0) += 1;
}
}
let mut top_actors: Vec<_> = actor_counts.into_iter().collect();
top_actors.sort_by_key(|b| std::cmp::Reverse(b.1));
top_actors.truncate(10);
SecurityReport {
period_start: since,
period_end: Utc::now(),
total_events,
by_severity,
by_category,
failures,
top_actors,
}
}
pub fn verify_chain(&self) -> Result<bool, AuditError> {
let buffer = self.event_buffer.lock().unwrap_or_else(|e| e.into_inner());
for i in 1..buffer.len() {
let prev = &buffer[i - 1];
let curr = &buffer[i];
let prev_hash = prev.compute_hash();
if curr.previous_hash.as_ref() != Some(&prev_hash) {
error!("Chain integrity violation detected at event {}", curr.id);
return Ok(false);
}
}
info!("Audit log chain integrity verified successfully");
Ok(true)
}
}
struct AnomalyDetector {
patterns: HashMap<String, EventPattern>,
anomaly_threshold: f64,
}
impl AnomalyDetector {
fn new() -> Self {
Self {
patterns: HashMap::new(),
anomaly_threshold: 0.8,
}
}
fn check_event(&mut self, event: &SecurityEvent) -> Option<String> {
let pattern_key = format!("{}:{}", event.category.clone() as u32, event.event_type);
let pattern = self
.patterns
.entry(pattern_key.clone())
.or_insert_with(EventPattern::new);
pattern.record(event);
if pattern.is_anomalous(event, self.anomaly_threshold) {
Some(format!(
"Anomalous {} event: {} (severity: {:?})",
event.category.clone() as u32,
event.event_type,
event.severity
))
} else {
None
}
}
}
struct EventPattern {
count: usize,
severity_counts: HashMap<String, usize>,
last_seen: DateTime<Utc>,
failure_rate: f64,
}
impl EventPattern {
fn new() -> Self {
Self {
count: 0,
severity_counts: HashMap::new(),
last_seen: Utc::now(),
failure_rate: 0.0,
}
}
fn record(&mut self, event: &SecurityEvent) {
self.count += 1;
self.last_seen = event.timestamp;
let sev_key = format!("{:?}", event.severity);
*self.severity_counts.entry(sev_key).or_insert(0) += 1;
if matches!(event.outcome, EventOutcome::Failure) {
self.failure_rate =
(self.failure_rate * (self.count - 1) as f64 + 1.0) / self.count as f64;
} else {
self.failure_rate = (self.failure_rate * (self.count - 1) as f64) / self.count as f64;
}
}
fn is_anomalous(&self, event: &SecurityEvent, _threshold: f64) -> bool {
if event.severity >= SecuritySeverity::High && self.count > 10 {
let sev_key = format!("{:?}", event.severity);
let sev_count = self.severity_counts.get(&sev_key).unwrap_or(&0);
let sev_ratio = *sev_count as f64 / self.count as f64;
if sev_ratio < 0.1 {
return true;
}
}
if matches!(event.outcome, EventOutcome::Failure) && self.failure_rate < 0.1 {
return true;
}
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityReport {
pub period_start: DateTime<Utc>,
pub period_end: DateTime<Utc>,
pub total_events: usize,
pub by_severity: HashMap<SecuritySeverity, usize>,
pub by_category: HashMap<SecurityCategory, usize>,
pub failures: usize,
pub top_actors: Vec<(String, usize)>,
}
pub fn log_authentication(
logger: &SecurityAuditLogger,
actor: String,
outcome: EventOutcome,
message: String,
) -> Result<(), AuditError> {
let severity = match outcome {
EventOutcome::Success => SecuritySeverity::Info,
EventOutcome::Failure => SecuritySeverity::Medium,
_ => SecuritySeverity::Low,
};
let event = SecurityEvent::new(
severity,
SecurityCategory::Authentication,
"user_login".to_string(),
message,
)
.with_actor(actor)
.with_outcome(outcome);
logger.log_event(event)
}
pub fn log_authorization(
logger: &SecurityAuditLogger,
actor: String,
resource: String,
outcome: EventOutcome,
message: String,
) -> Result<(), AuditError> {
let severity = match outcome {
EventOutcome::Failure => SecuritySeverity::Medium,
_ => SecuritySeverity::Info,
};
let event = SecurityEvent::new(
severity,
SecurityCategory::Authorization,
"access_check".to_string(),
message,
)
.with_actor(actor)
.with_resource(resource)
.with_outcome(outcome);
logger.log_event(event)
}
pub fn log_data_access(
logger: &SecurityAuditLogger,
actor: String,
resource: String,
message: String,
) -> Result<(), AuditError> {
let event = SecurityEvent::new(
SecuritySeverity::Info,
SecurityCategory::DataAccess,
"data_read".to_string(),
message,
)
.with_actor(actor)
.with_resource(resource);
logger.log_event(event)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_security_event_creation() {
let event = SecurityEvent::new(
SecuritySeverity::High,
SecurityCategory::Authentication,
"login_attempt".to_string(),
"User login failed".to_string(),
)
.with_actor("user1".to_string())
.with_outcome(EventOutcome::Failure);
assert_eq!(event.severity, SecuritySeverity::High);
assert_eq!(event.category, SecurityCategory::Authentication);
assert!(event.actor.is_some());
}
#[test]
fn test_event_hash() {
let event = SecurityEvent::new(
SecuritySeverity::Info,
SecurityCategory::System,
"test_event".to_string(),
"Test message".to_string(),
);
let hash1 = event.compute_hash();
let hash2 = event.compute_hash();
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64); }
#[test]
fn test_audit_logger() -> Result<(), AuditError> {
let temp_dir = env::temp_dir().join(format!("audit_test_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let config = AuditConfig {
log_dir: temp_dir.clone(),
enable_signatures: true,
..Default::default()
};
let logger = SecurityAuditLogger::new(config)?;
let event = SecurityEvent::new(
SecuritySeverity::Info,
SecurityCategory::System,
"test_event".to_string(),
"Test message".to_string(),
);
logger.log_event(event)?;
std::fs::remove_dir_all(&temp_dir).ok();
Ok(())
}
#[test]
fn test_chain_integrity() -> Result<(), AuditError> {
let temp_dir = env::temp_dir().join(format!("audit_chain_test_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let config = AuditConfig {
log_dir: temp_dir.clone(),
enable_signatures: true,
buffer_size: 100,
..Default::default()
};
let logger = SecurityAuditLogger::new(config)?;
for i in 0..10 {
let event = SecurityEvent::new(
SecuritySeverity::Info,
SecurityCategory::System,
format!("event_{}", i),
format!("Event {}", i),
);
logger.log_event(event)?;
}
assert!(logger.verify_chain()?);
std::fs::remove_dir_all(&temp_dir).ok();
Ok(())
}
#[test]
fn test_query_events() -> Result<(), AuditError> {
let temp_dir = env::temp_dir().join(format!("audit_query_test_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let config = AuditConfig {
log_dir: temp_dir.clone(),
buffer_size: 100,
..Default::default()
};
let logger = SecurityAuditLogger::new(config)?;
for i in 0..5 {
let event = SecurityEvent::new(
SecuritySeverity::Info,
SecurityCategory::Authentication,
format!("auth_{}", i),
format!("Auth event {}", i),
);
logger.log_event(event)?;
}
for i in 0..3 {
let event = SecurityEvent::new(
SecuritySeverity::Medium,
SecurityCategory::DataAccess,
format!("access_{}", i),
format!("Access event {}", i),
);
logger.log_event(event)?;
}
let auth_events =
logger.query_recent_events(10, Some(SecurityCategory::Authentication), None);
assert_eq!(auth_events.len(), 5);
let high_sev_events = logger.query_recent_events(10, None, Some(SecuritySeverity::Medium));
assert_eq!(high_sev_events.len(), 3);
std::fs::remove_dir_all(&temp_dir).ok();
Ok(())
}
}