use axum::{ body::Body, extract::State, http::{Method, Request, StatusCode}, middleware::Next, response::Response, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use crate::shared::state::AppState; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SopCategory { Authentication, Authorization, DataAccess, DataModification, DataDeletion, ConfigurationChange, SystemAccess, ApiAccess, FileAccess, UserManagement, RoleManagement, BillingOperation, ComplianceAction, SecurityEvent, AuditAction, BackupOperation, IncidentResponse, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SopSeverity { Info, Low, Medium, High, Critical, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SopOutcome { Success, Failure, Denied, Error, Timeout, Pending, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SopLogEntry { pub id: Uuid, pub timestamp: DateTime, pub category: SopCategory, pub severity: SopSeverity, pub outcome: SopOutcome, pub operation: String, pub description: String, pub user_id: Option, pub user_email: Option, pub organization_id: Option, pub resource_type: Option, pub resource_id: Option, pub ip_address: Option, pub user_agent: Option, pub request_method: Option, pub request_path: Option, pub request_id: Option, pub response_status: Option, pub duration_ms: Option, pub metadata: HashMap, pub compliance_controls: Vec, pub evidence_reference: Option, } impl SopLogEntry { pub fn new(category: SopCategory, operation: &str, description: &str) -> Self { Self { id: Uuid::new_v4(), timestamp: Utc::now(), category, severity: SopSeverity::Info, outcome: SopOutcome::Pending, operation: operation.to_string(), description: description.to_string(), user_id: None, user_email: None, organization_id: None, resource_type: None, resource_id: None, ip_address: None, user_agent: None, request_method: None, request_path: None, request_id: None, response_status: None, duration_ms: None, metadata: HashMap::new(), compliance_controls: Vec::new(), evidence_reference: None, } } pub fn with_severity(mut self, severity: SopSeverity) -> Self { self.severity = severity; self } pub fn with_outcome(mut self, outcome: SopOutcome) -> Self { self.outcome = outcome; self } pub fn with_user(mut self, user_id: Uuid, email: Option) -> Self { self.user_id = Some(user_id); self.user_email = email; self } pub fn with_organization(mut self, org_id: Uuid) -> Self { self.organization_id = Some(org_id); self } pub fn with_resource(mut self, resource_type: &str, resource_id: &str) -> Self { self.resource_type = Some(resource_type.to_string()); self.resource_id = Some(resource_id.to_string()); self } pub fn with_request_info( mut self, method: &str, path: &str, request_id: Option, ) -> Self { self.request_method = Some(method.to_string()); self.request_path = Some(path.to_string()); self.request_id = request_id; self } pub fn with_client_info(mut self, ip: Option, user_agent: Option) -> Self { self.ip_address = ip; self.user_agent = user_agent; self } pub fn with_response(mut self, status: u16, duration_ms: u64) -> Self { self.response_status = Some(status); self.duration_ms = Some(duration_ms); self } pub fn with_metadata(mut self, key: &str, value: serde_json::Value) -> Self { self.metadata.insert(key.to_string(), value); self } pub fn with_compliance_controls(mut self, controls: Vec<&str>) -> Self { self.compliance_controls = controls.into_iter().map(String::from).collect(); self } pub fn with_evidence_reference(mut self, reference: &str) -> Self { self.evidence_reference = Some(reference.to_string()); self } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SopSearchQuery { pub category: Option, pub severity: Option, pub outcome: Option, pub user_id: Option, pub organization_id: Option, pub resource_type: Option, pub from_date: Option>, pub to_date: Option>, pub operation_contains: Option, pub compliance_control: Option, pub page: u32, pub per_page: u32, } impl Default for SopSearchQuery { fn default() -> Self { Self { category: None, severity: None, outcome: None, user_id: None, organization_id: None, resource_type: None, from_date: None, to_date: None, operation_contains: None, compliance_control: None, page: 1, per_page: 50, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SopSearchResult { pub entries: Vec, pub total_count: u64, pub page: u32, pub per_page: u32, pub total_pages: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SopStatistics { pub total_entries: u64, pub entries_by_category: HashMap, pub entries_by_severity: HashMap, pub entries_by_outcome: HashMap, pub unique_users: u64, pub unique_organizations: u64, pub period_start: DateTime, pub period_end: DateTime, } pub struct SopLogger { entries: Arc>>, max_entries: usize, retention_days: i64, webhook_url: Option, siem_endpoint: Option, } impl Default for SopLogger { fn default() -> Self { Self::new() } } impl SopLogger { pub fn new() -> Self { Self { entries: Arc::new(RwLock::new(Vec::new())), max_entries: 100_000, retention_days: 365, webhook_url: None, siem_endpoint: None, } } pub fn with_max_entries(mut self, max: usize) -> Self { self.max_entries = max; self } pub fn with_retention_days(mut self, days: i64) -> Self { self.retention_days = days; self } pub fn with_webhook(mut self, url: String) -> Self { self.webhook_url = Some(url); self } pub fn with_siem_endpoint(mut self, endpoint: String) -> Self { self.siem_endpoint = Some(endpoint); self } pub async fn log(&self, entry: SopLogEntry) { let entry_clone = entry.clone(); { let mut entries = self.entries.write().await; if entries.len() >= self.max_entries { let remove_count = self.max_entries / 10; entries.drain(0..remove_count); } entries.push(entry); } self.log_to_persistent_storage(&entry_clone).await; if self.should_alert(&entry_clone) { self.send_alert(&entry_clone).await; } if let Some(ref webhook_url) = self.webhook_url { self.send_to_webhook(webhook_url, &entry_clone).await; } if let Some(ref siem_endpoint) = self.siem_endpoint { self.send_to_siem(siem_endpoint, &entry_clone).await; } } pub async fn log_authentication( &self, user_id: Option, email: Option, outcome: SopOutcome, method: &str, ip: Option, ) { let severity = match outcome { SopOutcome::Success => SopSeverity::Info, SopOutcome::Failure | SopOutcome::Denied => SopSeverity::Medium, _ => SopSeverity::Low, }; let mut entry = SopLogEntry::new( SopCategory::Authentication, "user_authentication", &format!("User authentication attempt via {method}"), ) .with_severity(severity) .with_outcome(outcome) .with_client_info(ip, None) .with_compliance_controls(vec!["CC6.1", "CC6.6"]); if let Some(uid) = user_id { entry = entry.with_user(uid, email); } self.log(entry).await; } pub async fn log_authorization( &self, user_id: Uuid, resource_type: &str, resource_id: &str, action: &str, outcome: SopOutcome, ) { let severity = match outcome { SopOutcome::Denied => SopSeverity::Medium, _ => SopSeverity::Info, }; let entry = SopLogEntry::new( SopCategory::Authorization, "authorization_check", &format!("Authorization check for {action} on {resource_type}"), ) .with_severity(severity) .with_outcome(outcome) .with_user(user_id, None) .with_resource(resource_type, resource_id) .with_metadata("action", serde_json::json!(action)) .with_compliance_controls(vec!["CC6.1", "CC6.3"]); self.log(entry).await; } pub async fn log_data_access( &self, user_id: Uuid, organization_id: Uuid, resource_type: &str, resource_id: &str, access_type: &str, ) { let entry = SopLogEntry::new( SopCategory::DataAccess, "data_access", &format!("{access_type} access to {resource_type}"), ) .with_severity(SopSeverity::Info) .with_outcome(SopOutcome::Success) .with_user(user_id, None) .with_organization(organization_id) .with_resource(resource_type, resource_id) .with_metadata("access_type", serde_json::json!(access_type)) .with_compliance_controls(vec!["CC6.1", "CC6.5", "PI1.1"]); self.log(entry).await; } pub async fn log_data_modification( &self, user_id: Uuid, organization_id: Uuid, resource_type: &str, resource_id: &str, modification_type: &str, changes: Option, ) { let mut entry = SopLogEntry::new( SopCategory::DataModification, "data_modification", &format!("{modification_type} on {resource_type}"), ) .with_severity(SopSeverity::Medium) .with_outcome(SopOutcome::Success) .with_user(user_id, None) .with_organization(organization_id) .with_resource(resource_type, resource_id) .with_metadata("modification_type", serde_json::json!(modification_type)) .with_compliance_controls(vec!["CC6.1", "CC7.2", "PI1.4"]); if let Some(c) = changes { entry = entry.with_metadata("changes", c); } self.log(entry).await; } pub async fn log_configuration_change( &self, user_id: Uuid, config_type: &str, config_key: &str, old_value: Option<&str>, new_value: Option<&str>, ) { let mut entry = SopLogEntry::new( SopCategory::ConfigurationChange, "configuration_change", &format!("Configuration change: {config_type}/{config_key}"), ) .with_severity(SopSeverity::High) .with_outcome(SopOutcome::Success) .with_user(user_id, None) .with_resource(config_type, config_key) .with_compliance_controls(vec!["CC6.1", "CC7.1", "CC8.1"]); if let Some(old) = old_value { entry = entry.with_metadata("old_value", serde_json::json!(old)); } if let Some(new) = new_value { entry = entry.with_metadata("new_value", serde_json::json!(new)); } self.log(entry).await; } pub async fn log_security_event( &self, event_type: &str, description: &str, severity: SopSeverity, metadata: HashMap, ) { let mut entry = SopLogEntry::new(SopCategory::SecurityEvent, event_type, description) .with_severity(severity) .with_outcome(SopOutcome::Success) .with_compliance_controls(vec!["CC7.2", "CC7.3", "CC7.4"]); for (key, value) in metadata { entry = entry.with_metadata(&key, value); } self.log(entry).await; } pub async fn log_incident_response( &self, incident_id: &str, action: &str, responder_id: Uuid, details: &str, ) { let entry = SopLogEntry::new( SopCategory::IncidentResponse, action, &format!("Incident response action: {details}"), ) .with_severity(SopSeverity::High) .with_outcome(SopOutcome::Success) .with_user(responder_id, None) .with_resource("incident", incident_id) .with_compliance_controls(vec!["CC7.3", "CC7.4", "CC7.5"]); self.log(entry).await; } pub async fn log_backup_operation( &self, operation: &str, backup_id: &str, outcome: SopOutcome, size_bytes: Option, ) { let mut entry = SopLogEntry::new( SopCategory::BackupOperation, operation, &format!("Backup operation: {operation}"), ) .with_severity(SopSeverity::Medium) .with_outcome(outcome) .with_resource("backup", backup_id) .with_compliance_controls(vec!["A1.2", "A1.3"]); if let Some(size) = size_bytes { entry = entry.with_metadata("size_bytes", serde_json::json!(size)); } self.log(entry).await; } pub async fn search(&self, query: SopSearchQuery) -> SopSearchResult { let entries = self.entries.read().await; let filtered: Vec<_> = entries .iter() .filter(|e| { if let Some(ref cat) = query.category { if &e.category != cat { return false; } } if let Some(ref sev) = query.severity { if &e.severity != sev { return false; } } if let Some(ref out) = query.outcome { if &e.outcome != out { return false; } } if let Some(uid) = query.user_id { if e.user_id != Some(uid) { return false; } } if let Some(oid) = query.organization_id { if e.organization_id != Some(oid) { return false; } } if let Some(ref rt) = query.resource_type { if e.resource_type.as_ref() != Some(rt) { return false; } } if let Some(from) = query.from_date { if e.timestamp < from { return false; } } if let Some(to) = query.to_date { if e.timestamp > to { return false; } } if let Some(ref op) = query.operation_contains { if !e.operation.to_lowercase().contains(&op.to_lowercase()) { return false; } } if let Some(ref ctrl) = query.compliance_control { if !e.compliance_controls.contains(ctrl) { return false; } } true }) .cloned() .collect(); let total_count = filtered.len() as u64; let total_pages = ((total_count as f64) / (query.per_page as f64)).ceil() as u32; let start = ((query.page - 1) * query.per_page) as usize; let end = (start + query.per_page as usize).min(filtered.len()); let page_entries = if start < filtered.len() { filtered[start..end].to_vec() } else { Vec::new() }; SopSearchResult { entries: page_entries, total_count, page: query.page, per_page: query.per_page, total_pages, } } pub async fn get_statistics( &self, from_date: DateTime, to_date: DateTime, ) -> SopStatistics { let entries = self.entries.read().await; let filtered: Vec<_> = entries .iter() .filter(|e| e.timestamp >= from_date && e.timestamp <= to_date) .collect(); let mut by_category: HashMap = HashMap::new(); let mut by_severity: HashMap = HashMap::new(); let mut by_outcome: HashMap = HashMap::new(); let mut unique_users = std::collections::HashSet::new(); let mut unique_orgs = std::collections::HashSet::new(); for entry in &filtered { *by_category .entry(format!("{:?}", entry.category)) .or_insert(0) += 1; *by_severity .entry(format!("{:?}", entry.severity)) .or_insert(0) += 1; *by_outcome .entry(format!("{:?}", entry.outcome)) .or_insert(0) += 1; if let Some(uid) = entry.user_id { unique_users.insert(uid); } if let Some(oid) = entry.organization_id { unique_orgs.insert(oid); } } SopStatistics { total_entries: filtered.len() as u64, entries_by_category: by_category, entries_by_severity: by_severity, entries_by_outcome: by_outcome, unique_users: unique_users.len() as u64, unique_organizations: unique_orgs.len() as u64, period_start: from_date, period_end: to_date, } } pub async fn export_for_audit( &self, from_date: DateTime, to_date: DateTime, compliance_control: Option<&str>, ) -> Vec { let entries = self.entries.read().await; entries .iter() .filter(|e| { if e.timestamp < from_date || e.timestamp > to_date { return false; } if let Some(ctrl) = compliance_control { if !e.compliance_controls.contains(&ctrl.to_string()) { return false; } } true }) .cloned() .collect() } pub async fn cleanup_old_entries(&self) { let cutoff = Utc::now() - chrono::Duration::days(self.retention_days); let mut entries = self.entries.write().await; entries.retain(|e| e.timestamp > cutoff); } fn should_alert(&self, entry: &SopLogEntry) -> bool { matches!(entry.severity, SopSeverity::High | SopSeverity::Critical) || matches!(entry.outcome, SopOutcome::Denied | SopOutcome::Error) || matches!( entry.category, SopCategory::SecurityEvent | SopCategory::IncidentResponse ) } async fn send_alert(&self, entry: &SopLogEntry) { log::warn!( "SOP ALERT: [{:?}] {:?} - {} - {}", entry.severity, entry.category, entry.operation, entry.description ); } async fn log_to_persistent_storage(&self, entry: &SopLogEntry) { log::info!( "SOP LOG: [{:?}] {:?}/{:?} - {} - {}", entry.severity, entry.category, entry.outcome, entry.operation, entry.description ); } async fn send_to_webhook(&self, webhook_url: &str, entry: &SopLogEntry) { log::debug!("Sending SOP entry to webhook: {}", webhook_url); let _ = entry; } async fn send_to_siem(&self, siem_endpoint: &str, entry: &SopLogEntry) { log::debug!("Sending SOP entry to SIEM: {}", siem_endpoint); let _ = entry; } } fn categorize_request(method: &Method, path: &str) -> SopCategory { let path_lower = path.to_lowercase(); if path_lower.contains("/auth") || path_lower.contains("/login") || path_lower.contains("/logout") { return SopCategory::Authentication; } if path_lower.contains("/users") || path_lower.contains("/members") { return SopCategory::UserManagement; } if path_lower.contains("/roles") || path_lower.contains("/permissions") { return SopCategory::RoleManagement; } if path_lower.contains("/billing") || path_lower.contains("/subscription") || path_lower.contains("/invoice") { return SopCategory::BillingOperation; } if path_lower.contains("/compliance") || path_lower.contains("/audit") { return SopCategory::ComplianceAction; } if path_lower.contains("/settings") || path_lower.contains("/config") { return SopCategory::ConfigurationChange; } if path_lower.contains("/files") || path_lower.contains("/documents") || path_lower.contains("/drive") { return SopCategory::FileAccess; } match *method { Method::GET | Method::HEAD => SopCategory::DataAccess, Method::POST => SopCategory::DataModification, Method::PUT | Method::PATCH => SopCategory::DataModification, Method::DELETE => SopCategory::DataDeletion, _ => SopCategory::ApiAccess, } } fn determine_severity(method: &Method, path: &str, status: StatusCode) -> SopSeverity { if status.is_server_error() { return SopSeverity::High; } if status == StatusCode::FORBIDDEN || status == StatusCode::UNAUTHORIZED { return SopSeverity::Medium; } let path_lower = path.to_lowercase(); if path_lower.contains("/admin") || path_lower.contains("/security") { return SopSeverity::Medium; } if *method == Method::DELETE { return SopSeverity::Medium; } if path_lower.contains("/config") || path_lower.contains("/settings") { if *method != Method::GET { return SopSeverity::Medium; } } SopSeverity::Info } fn determine_outcome(status: StatusCode) -> SopOutcome { if status.is_success() { SopOutcome::Success } else if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN { SopOutcome::Denied } else if status.is_server_error() { SopOutcome::Error } else if status == StatusCode::REQUEST_TIMEOUT || status == StatusCode::GATEWAY_TIMEOUT { SopOutcome::Timeout } else { SopOutcome::Failure } } pub async fn sop_logging_middleware( State(state): State>, request: Request, next: Next, ) -> Response { let start_time = std::time::Instant::now(); let request_id = Uuid::new_v4().to_string(); let method = request.method().clone(); let path = request.uri().path().to_string(); let user_agent = request .headers() .get("user-agent") .and_then(|v| v.to_str().ok()) .map(String::from); let ip_address = request .headers() .get("x-forwarded-for") .or_else(|| request.headers().get("x-real-ip")) .and_then(|v| v.to_str().ok()) .map(String::from); let user_id = request .headers() .get("x-user-id") .and_then(|v| v.to_str().ok()) .and_then(|s| Uuid::parse_str(s).ok()); let organization_id = request .headers() .get("x-organization-id") .and_then(|v| v.to_str().ok()) .and_then(|s| Uuid::parse_str(s).ok()); let response = next.run(request).await; let duration_ms = start_time.elapsed().as_millis() as u64; let status = response.status(); let category = categorize_request(&method, &path); let severity = determine_severity(&method, &path, status); let outcome = determine_outcome(status); let mut entry = SopLogEntry::new( category, &format!("{}_{}", method.as_str().to_lowercase(), sanitize_path(&path)), &format!("{} {}", method, path), ) .with_severity(severity) .with_outcome(outcome) .with_request_info(method.as_str(), &path, Some(request_id)) .with_client_info(ip_address, user_agent) .with_response(status.as_u16(), duration_ms); if let Some(uid) = user_id { entry = entry.with_user(uid, None); } if let Some(oid) = organization_id { entry = entry.with_organization(oid); } let logger = SopLogger::new(); logger.log(entry).await; drop(state); response } fn sanitize_path(path: &str) -> String { let uuid_pattern = regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}").ok(); let numeric_pattern = regex::Regex::new(r"/\d+").ok(); let mut sanitized = path.to_string(); if let Some(re) = uuid_pattern { sanitized = re.replace_all(&sanitized, ":id").to_string(); } if let Some(re) = numeric_pattern { sanitized = re.replace_all(&sanitized, "/:id").to_string(); } sanitized .replace('/', "_") .trim_matches('_') .to_lowercase() } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_sop_log_entry_creation() { let entry = SopLogEntry::new( SopCategory::Authentication, "user_login", "User logged in", ) .with_severity(SopSeverity::Info) .with_outcome(SopOutcome::Success) .with_user(Uuid::new_v4(), Some("test@example.com".to_string())); assert_eq!(entry.category, SopCategory::Authentication); assert_eq!(entry.severity, SopSeverity::Info); assert_eq!(entry.outcome, SopOutcome::Success); assert!(entry.user_id.is_some()); assert_eq!(entry.user_email, Some("test@example.com".to_string())); } #[tokio::test] async fn test