use chrono::{DateTime, Duration, Timelike, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{info, warn}; use uuid::Uuid; const DEFAULT_BRUTE_FORCE_THRESHOLD: u32 = 5; const DEFAULT_BRUTE_FORCE_WINDOW_SECONDS: i64 = 300; const DEFAULT_LOCKOUT_DURATION_MINUTES: i64 = 30; const DEFAULT_ANOMALY_THRESHOLD: f64 = 3.0; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecurityMonitoringConfig { pub enabled: bool, pub brute_force_threshold: u32, pub brute_force_window_seconds: i64, pub lockout_duration_minutes: i64, pub anomaly_detection_enabled: bool, pub anomaly_threshold_stddev: f64, pub geo_anomaly_detection: bool, pub impossible_travel_detection: bool, pub max_travel_speed_kmh: f64, pub alert_on_critical: bool, pub alert_on_high: bool, pub retention_hours: u32, } impl Default for SecurityMonitoringConfig { fn default() -> Self { Self { enabled: true, brute_force_threshold: DEFAULT_BRUTE_FORCE_THRESHOLD, brute_force_window_seconds: DEFAULT_BRUTE_FORCE_WINDOW_SECONDS, lockout_duration_minutes: DEFAULT_LOCKOUT_DURATION_MINUTES, anomaly_detection_enabled: true, anomaly_threshold_stddev: DEFAULT_ANOMALY_THRESHOLD, geo_anomaly_detection: true, impossible_travel_detection: true, max_travel_speed_kmh: 1000.0, alert_on_critical: true, alert_on_high: true, retention_hours: 168, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SecurityEventType { LoginAttempt, LoginSuccess, LoginFailure, PasswordReset, MfaChallenge, MfaFailure, SessionCreated, SessionRevoked, PermissionDenied, RateLimitExceeded, SuspiciousActivity, BruteForceDetected, AccountLocked, IpBlocked, GeoAnomalyDetected, ImpossibleTravel, NewDeviceLogin, ApiKeyUsed, PrivilegeEscalation, DataExfiltration, } impl SecurityEventType { pub fn as_str(&self) -> &'static str { match self { Self::LoginAttempt => "login_attempt", Self::LoginSuccess => "login_success", Self::LoginFailure => "login_failure", Self::PasswordReset => "password_reset", Self::MfaChallenge => "mfa_challenge", Self::MfaFailure => "mfa_failure", Self::SessionCreated => "session_created", Self::SessionRevoked => "session_revoked", Self::PermissionDenied => "permission_denied", Self::RateLimitExceeded => "rate_limit_exceeded", Self::SuspiciousActivity => "suspicious_activity", Self::BruteForceDetected => "brute_force_detected", Self::AccountLocked => "account_locked", Self::IpBlocked => "ip_blocked", Self::GeoAnomalyDetected => "geo_anomaly_detected", Self::ImpossibleTravel => "impossible_travel", Self::NewDeviceLogin => "new_device_login", Self::ApiKeyUsed => "api_key_used", Self::PrivilegeEscalation => "privilege_escalation", Self::DataExfiltration => "data_exfiltration", } } pub fn severity(&self) -> AlertSeverity { match self { Self::BruteForceDetected | Self::AccountLocked | Self::PrivilegeEscalation | Self::DataExfiltration => AlertSeverity::Critical, Self::LoginFailure | Self::MfaFailure | Self::PermissionDenied | Self::ImpossibleTravel | Self::GeoAnomalyDetected => AlertSeverity::High, Self::RateLimitExceeded | Self::SuspiciousActivity | Self::IpBlocked | Self::NewDeviceLogin => AlertSeverity::Medium, _ => AlertSeverity::Low, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)] pub enum AlertSeverity { Low, Medium, High, Critical, } impl AlertSeverity { pub fn as_str(&self) -> &'static str { match self { Self::Low => "low", Self::Medium => "medium", Self::High => "high", Self::Critical => "critical", } } pub fn score(&self) -> u8 { match self { Self::Low => 25, Self::Medium => 50, Self::High => 75, Self::Critical => 100, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecurityEvent { pub id: Uuid, pub timestamp: DateTime, pub event_type: SecurityEventType, pub severity: AlertSeverity, pub user_id: Option, pub ip_address: Option, pub user_agent: Option, pub location: Option, pub device_fingerprint: Option, pub details: HashMap, pub request_id: Option, } impl SecurityEvent { pub fn new(event_type: SecurityEventType) -> Self { Self { id: Uuid::new_v4(), timestamp: Utc::now(), event_type, severity: event_type.severity(), user_id: None, ip_address: None, user_agent: None, location: None, device_fingerprint: None, details: HashMap::new(), request_id: None, } } pub fn with_user(mut self, user_id: Uuid) -> Self { self.user_id = Some(user_id); self } pub fn with_ip(mut self, ip: String) -> Self { self.ip_address = Some(ip); self } pub fn with_user_agent(mut self, ua: String) -> Self { self.user_agent = Some(ua); self } pub fn with_location(mut self, location: GeoLocation) -> Self { self.location = Some(location); self } pub fn with_device(mut self, fingerprint: String) -> Self { self.device_fingerprint = Some(fingerprint); self } pub fn with_detail(mut self, key: &str, value: serde_json::Value) -> Self { self.details.insert(key.to_string(), value); self } pub fn with_request_id(mut self, request_id: String) -> Self { self.request_id = Some(request_id); self } pub fn is_critical(&self) -> bool { self.severity == AlertSeverity::Critical } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeoLocation { pub country: Option, pub region: Option, pub city: Option, pub latitude: Option, pub longitude: Option, pub timezone: Option, } impl GeoLocation { pub fn new() -> Self { Self { country: None, region: None, city: None, latitude: None, longitude: None, timezone: None, } } pub fn with_country(mut self, country: &str) -> Self { self.country = Some(country.to_string()); self } pub fn with_city(mut self, city: &str) -> Self { self.city = Some(city.to_string()); self } pub fn with_coordinates(mut self, lat: f64, lon: f64) -> Self { self.latitude = Some(lat); self.longitude = Some(lon); self } pub fn distance_km(&self, other: &GeoLocation) -> Option { match (self.latitude, self.longitude, other.latitude, other.longitude) { (Some(lat1), Some(lon1), Some(lat2), Some(lon2)) => { Some(haversine_distance(lat1, lon1, lat2, lon2)) } _ => None, } } } impl Default for GeoLocation { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginAttemptRecord { pub user_id: Option, pub ip_address: String, pub timestamp: DateTime, pub success: bool, pub user_agent: Option, pub location: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LockoutRecord { pub identifier: String, pub locked_at: DateTime, pub expires_at: DateTime, pub reason: String, pub attempt_count: u32, } impl LockoutRecord { pub fn is_expired(&self) -> bool { Utc::now() > self.expires_at } pub fn remaining_time(&self) -> Duration { if self.is_expired() { Duration::zero() } else { self.expires_at - Utc::now() } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecurityAlert { pub id: Uuid, pub timestamp: DateTime, pub severity: AlertSeverity, pub title: String, pub description: String, pub event_ids: Vec, pub user_id: Option, pub ip_address: Option, pub acknowledged: bool, pub acknowledged_by: Option, pub acknowledged_at: Option>, pub resolved: bool, pub resolved_at: Option>, } impl SecurityAlert { pub fn new(severity: AlertSeverity, title: &str, description: &str) -> Self { Self { id: Uuid::new_v4(), timestamp: Utc::now(), severity, title: title.to_string(), description: description.to_string(), event_ids: Vec::new(), user_id: None, ip_address: None, acknowledged: false, acknowledged_by: None, acknowledged_at: None, resolved: false, resolved_at: None, } } pub fn with_event(mut self, event_id: Uuid) -> Self { self.event_ids.push(event_id); self } pub fn with_events(mut self, event_ids: Vec) -> Self { self.event_ids.extend(event_ids); self } pub fn with_user(mut self, user_id: Uuid) -> Self { self.user_id = Some(user_id); self } pub fn with_ip(mut self, ip: String) -> Self { self.ip_address = Some(ip); self } pub fn acknowledge(&mut self, by: Uuid) { self.acknowledged = true; self.acknowledged_by = Some(by); self.acknowledged_at = Some(Utc::now()); } pub fn resolve(&mut self) { self.resolved = true; self.resolved_at = Some(Utc::now()); } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserSecurityProfile { pub user_id: Uuid, pub known_ips: Vec, pub known_devices: Vec, pub known_locations: Vec, pub last_login: Option>, pub last_location: Option, pub login_times: Vec, pub risk_score: f64, pub is_locked: bool, pub lock_expires_at: Option>, } impl UserSecurityProfile { pub fn new(user_id: Uuid) -> Self { Self { user_id, known_ips: Vec::new(), known_devices: Vec::new(), known_locations: Vec::new(), last_login: None, last_location: None, login_times: Vec::new(), risk_score: 0.0, is_locked: false, lock_expires_at: None, } } pub fn is_known_ip(&self, ip: &str) -> bool { self.known_ips.contains(&ip.to_string()) } pub fn is_known_device(&self, device: &str) -> bool { self.known_devices.contains(&device.to_string()) } pub fn add_known_ip(&mut self, ip: &str) { if !self.is_known_ip(ip) { self.known_ips.push(ip.to_string()); if self.known_ips.len() > 100 { self.known_ips.remove(0); } } } pub fn add_known_device(&mut self, device: &str) { if !self.is_known_device(device) { self.known_devices.push(device.to_string()); if self.known_devices.len() > 50 { self.known_devices.remove(0); } } } pub fn record_login(&mut self, location: Option) { let now = Utc::now(); self.last_login = Some(now); self.login_times.push(now.hour()); if self.login_times.len() > 1000 { self.login_times.remove(0); } if let Some(loc) = location { self.last_location = Some(loc.clone()); self.known_locations.push(loc); if self.known_locations.len() > 50 { self.known_locations.remove(0); } } } pub fn is_unusual_login_time(&self, hour: u32) -> bool { if self.login_times.len() < 10 { return false; } let count = self.login_times.iter().filter(|&&h| h == hour).count(); let percentage = count as f64 / self.login_times.len() as f64; percentage < 0.01 } pub fn lock(&mut self, duration: Duration) { self.is_locked = true; self.lock_expires_at = Some(Utc::now() + duration); } pub fn unlock(&mut self) { self.is_locked = false; self.lock_expires_at = None; } pub fn check_lock_status(&mut self) -> bool { if self.is_locked { if let Some(expires) = self.lock_expires_at { if Utc::now() > expires { self.unlock(); return false; } } return true; } false } } pub struct SecurityMonitor { config: SecurityMonitoringConfig, events: Arc>>, login_attempts: Arc>>>, lockouts: Arc>>, alerts: Arc>>, user_profiles: Arc>>, blocked_ips: Arc>>>, } impl SecurityMonitor { pub fn new(config: SecurityMonitoringConfig) -> Self { Self { config, events: Arc::new(RwLock::new(Vec::new())), login_attempts: Arc::new(RwLock::new(HashMap::new())), lockouts: Arc::new(RwLock::new(HashMap::new())), alerts: Arc::new(RwLock::new(Vec::new())), user_profiles: Arc::new(RwLock::new(HashMap::new())), blocked_ips: Arc::new(RwLock::new(HashMap::new())), } } pub fn with_defaults() -> Self { Self::new(SecurityMonitoringConfig::default()) } pub async fn record_event(&self, event: SecurityEvent) { if !self.config.enabled { return; } let should_alert = match event.severity { AlertSeverity::Critical => self.config.alert_on_critical, AlertSeverity::High => self.config.alert_on_high, _ => false, }; if should_alert { self.create_alert_from_event(&event).await; } let mut events = self.events.write().await; events.push(event); if events.len() > 100_000 { events.remove(0); } } pub async fn record_login_attempt( &self, user_id: Option, ip: &str, success: bool, user_agent: Option<&str>, location: Option, ) -> Option { if !self.config.enabled { return None; } let record = LoginAttemptRecord { user_id, ip_address: ip.to_string(), timestamp: Utc::now(), success, user_agent: user_agent.map(String::from), location: location.clone(), }; let key = user_id .map(|id| id.to_string()) .unwrap_or_else(|| ip.to_string()); let mut attempts = self.login_attempts.write().await; let user_attempts = attempts.entry(key.clone()).or_default(); user_attempts.push(record); let window_start = Utc::now() - Duration::seconds(self.config.brute_force_window_seconds); user_attempts.retain(|a| a.timestamp > window_start); let failed_count = user_attempts.iter().filter(|a| !a.success).count() as u32; drop(attempts); let event_type = if success { SecurityEventType::LoginSuccess } else { SecurityEventType::LoginFailure }; let mut event = SecurityEvent::new(event_type).with_ip(ip.to_string()); if let Some(uid) = user_id { event = event.with_user(uid); } if let Some(ua) = user_agent { event = event.with_user_agent(ua.to_string()); } if let Some(loc) = location.clone() { event = event.with_location(loc); } self.record_event(event).await; if !success && failed_count >= self.config.brute_force_threshold { return self.handle_brute_force(&key, ip, user_id).await; } if success { if let Some(uid) = user_id { self.check_login_anomalies(uid, ip, location).await; } } None } async fn handle_brute_force( &self, key: &str, ip: &str, user_id: Option, ) -> Option { let lockout = LockoutRecord { identifier: key.to_string(), locked_at: Utc::now(), expires_at: Utc::now() + Duration::minutes(self.config.lockout_duration_minutes), reason: "Brute force attack detected".to_string(), attempt_count: self.config.brute_force_threshold, }; { let mut lockouts = self.lockouts.write().await; lockouts.insert(key.to_string(), lockout); } { let mut blocked = self.blocked_ips.write().await; blocked.insert( ip.to_string(), Utc::now() + Duration::minutes(self.config.lockout_duration_minutes), ); } if let Some(uid) = user_id { let mut profiles = self.user_profiles.write().await; let profile = profiles .entry(uid) .or_insert_with(|| UserSecurityProfile::new(uid)); profile.lock(Duration::minutes(self.config.lockout_duration_minutes)); } let mut event = SecurityEvent::new(SecurityEventType::BruteForceDetected) .with_ip(ip.to_string()) .with_detail( "threshold", serde_json::json!(self.config.brute_force_threshold), ); if let Some(uid) = user_id { event = event.with_user(uid); } self.record_event(event.clone()).await; warn!( "Brute force attack detected for {} from IP {}", key, ip ); let alert = SecurityAlert::new( AlertSeverity::Critical, "Brute Force Attack Detected", &format!( "Multiple failed login attempts detected for {}. Account locked for {} minutes.", key, self.config.lockout_duration_minutes ), ) .with_event(event.id) .with_ip(ip.to_string()); let alert_with_user = if let Some(uid) = user_id { alert.with_user(uid) } else { alert }; let mut alerts = self.alerts.write().await; alerts.push(alert_with_user.clone()); Some(alert_with_user) } async fn check_login_anomalies( &self, user_id: Uuid, ip: &str, location: Option, ) { if !self.config.anomaly_detection_enabled { return; } let is_new_ip = { let profiles = self.user_profiles.read().await; profiles .get(&user_id) .map(|p| !p.is_known_ip(ip)) .unwrap_or(true) }; if is_new_ip { let event = SecurityEvent::new(SecurityEventType::NewDeviceLogin) .with_user(user_id) .with_ip(ip.to_string()) .with_detail("reason", serde_json::json!("new_ip")); self.record_event(event).await; let mut profiles = self.user_profiles.write().await; let profile = profiles .entry(user_id) .or_insert_with(|| UserSecurityProfile::new(user_id)); profile.add_known_ip(ip); } let mut profiles = self.user_profiles.write().await; let profile = profiles .entry(user_id) .or_insert_with(|| UserSecurityProfile::new(user_id)); if self.config.impossible_travel_detection { if let (Some(last_loc), Some(current_loc)) = (profile.last_location.as_ref(), location.as_ref()) { if let Some(last_login) = profile.last_login { if let Some(distance) = last_loc.distance_km(current_loc) { let time_diff = (Utc::now() - last_login).num_hours().max(1) as f64; let speed = distance / time_diff; if speed > self.config.max_travel_speed_kmh { let event = SecurityEvent::new(SecurityEventType::ImpossibleTravel) .with_user(user_id) .with_ip(ip.to_string()) .with_location(current_loc.clone()) .with_detail("distance_km", serde_json::json!(distance)) .with_detail("speed_kmh", serde_json::json!(speed)); let event_to_record = event; drop(profiles); self.record_event(event_to_record).await; warn!( "Impossible travel detected for user {}: {} km in {} hours", user_id, distance, time_diff ); return; } } } } } if self.config.geo_anomaly_detection { if let Some(current_loc) = location.as_ref() { if let Some(ref country) = current_loc.country { let known_countries: Vec = profile .known_locations .iter() .filter_map(|l| l.country.clone()) .collect(); if !known_countries.is_empty() && !known_countries.contains(country) { let event = SecurityEvent::new(SecurityEventType::GeoAnomalyDetected) .with_user(user_id) .with_ip(ip.to_string()) .with_location(current_loc.clone()) .with_detail("new_country", serde_json::json!(country)); drop(profiles); self.record_event(event).await; return; } } } } let profile = profiles .entry(user_id) .or_insert_with(|| UserSecurityProfile::new(user_id)); profile.record_login(location); } pub async fn is_locked(&self, identifier: &str) -> bool { let lockouts = self.lockouts.read().await; if let Some(lockout) = lockouts.get(identifier) { !lockout.is_expired() } else { false } } pub async fn is_ip_blocked(&self, ip: &str) -> bool { let blocked = self.blocked_ips.read().await; if let Some(expires) = blocked.get(ip) { Utc::now() < *expires } else { false } } pub async fn get_lockout_info(&self, identifier: &str) -> Option { let lockouts = self.lockouts.read().await; lockouts.get(identifier).cloned() } pub async fn unlock(&self, identifier: &str) -> bool { let mut lockouts = self.lockouts.write().await; lockouts.remove(identifier).is_some() } pub async fn unblock_ip(&self, ip: &str) -> bool { let mut blocked = self.blocked_ips.write().await; blocked.remove(ip).is_some() } pub async fn block_ip(&self, ip: &str, duration: Duration, reason: &str) { let mut blocked = self.blocked_ips.write().await; blocked.insert(ip.to_string(), Utc::now() + duration); let event = SecurityEvent::new(SecurityEventType::IpBlocked) .with_ip(ip.to_string()) .with_detail("reason", serde_json::json!(reason)) .with_detail("duration_minutes", serde_json::json!(duration.num_minutes())); drop(blocked); self.record_event(event).await; info!("IP {} blocked for {} minutes: {}", ip, duration.num_minutes(), reason); } async fn create_alert_from_event(&self, event: &SecurityEvent) { let alert = SecurityAlert::new( event.severity, &format!("Security Event: {}", event.event_type.as_str()), &format!( "{} event detected{}{}", event.event_type.as_str(), event .user_id .map(|id| format!(" for user {}", id)) .unwrap_or_default(), event .ip_address .as_ref() .map(|ip| format!(" from IP {}", ip)) .unwrap_or_default() ), ) .with_event(event.id); let alert_with_user = if let Some(uid) = event.user_id { alert.with_user(uid) } else { alert }; let alert_with_ip = if let Some(ref ip) = event.ip_address { alert_with_user.with_ip(ip.clone()) } else { alert_with_user }; let mut alerts = self.alerts.write().await; alerts.push(alert_with_ip); } pub async fn get_alerts(&self, unacknowledged_only: bool, limit: usize) -> Vec { let alerts = self.alerts.read().await; let filtered: Vec = if unacknowledged_only { alerts.iter().filter(|a| !a.acknowledged).cloned().collect() } else { alerts.clone() }; filtered.into_iter().rev().take(limit).collect() } pub async fn acknowledge_alert(&self, alert_id: Uuid, by: Uuid) -> bool { let mut alerts = self.alerts.write().await; if let Some(alert) = alerts.iter_mut().find(|a| a.id == alert_id) { alert.acknowledge(by); true } else { false } } pub async fn resolve_alert(&self, alert_id: Uuid) -> bool { let mut alerts = self.alerts.write().await; if let Some(alert) = alerts.iter_mut().find(|a| a.id == alert_id) { alert.resolve(); true } else { false } } pub async fn get_user_profile(&self, user_id: Uuid) -> Option { let profiles = self.user_profiles.read().await; profiles.get(&user_id).cloned() } pub async fn get_recent_events( &self, event_type: Option, user_id: Option, limit: usize, ) -> Vec { let events = self.events.read().await; let filtered: Vec = events .iter() .filter(|e| { if let Some(et) = event_type { if e.event_type != et { return false; } } if let Some(uid) = user_id { if e.user_id != Some(uid) { return false; } } true }) .cloned() .collect(); filtered.into_iter().rev().take(limit).collect() } pub async fn cleanup_old_data(&self) -> usize { let cutoff = Utc::now() - Duration::hours(self.config.retention_hours as i64); let mut total_cleaned = 0; { let mut events = self.events.write().await; let initial = events.len(); events.retain(|e| e.timestamp > cutoff); total_cleaned += initial - events.len(); } { let mut attempts = self.login_attempts.write().await; for records in attempts.values_mut() { let initial = records.len(); records.retain(|r| r.timestamp > cutoff); total_cleaned += initial - records.len(); } } { let mut lockouts = self.lockouts.write().await; let initial = lockouts.len(); lockouts.retain(|_, l| !l.is_expired()); total_cleaned += initial - lockouts.len(); } { let mut blocked = self.blocked_ips.write().await; let initial = blocked.len(); blocked.retain(|_, expires| Utc::now() < *expires); total_cleaned += initial - blocked.len(); } if total_cleaned > 0 { info!("Cleaned up {} old security monitoring records", total_cleaned); } total_cleaned } pub fn config(&self) -> &SecurityMonitoringConfig { &self.config } } fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 { const EARTH_RADIUS_KM: f64 = 6371.0; let lat1_rad = lat1.to_radians(); let lat2_rad = lat2.to_radians(); let delta_lat = (lat2 - lat1).to_radians(); let delta_lon = (lon2 - lon1).to_radians(); let a = (delta_lat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().asin(); EARTH_RADIUS_KM * c } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_record_login_success() { let monitor = SecurityMonitor::with_defaults(); let user_id = Uuid::new_v4(); let alert = monitor .record_login_attempt(Some(user_id), "192.168.1.1", true, Some("TestAgent"), None) .await; assert!(alert.is_none()); let events = monitor.get_recent_events(Some(SecurityEventType::LoginSuccess), None, 10).await; assert!(!events.is_empty()); } #[tokio::test] async fn test_brute_force_detection() { let mut config = SecurityMonitoringConfig::default(); config.brute_force_threshold = 3; let monitor = SecurityMonitor::new(config); let user_id = Uuid::new_v4(); for _ in 0..2 { let alert = monitor .record_login_attempt(Some(user_id), "10.0.0.1", false, None, None) .await; assert!(alert.is_none()); } let alert = monitor .record_login_attempt(Some(user_id), "10.0.0.1", false, None, None) .await; assert!(alert.is_some()); assert_eq!(alert.unwrap().severity, AlertSeverity::Critical); } #[tokio::test] async fn test_lockout() { let mut config = SecurityMonitoringConfig::default(); config.brute_force_threshold = 2; let monitor = SecurityMonitor::new(config); let user_id = Uuid::new_v4(); let identifier = user_id.to_string(); monitor .record_login_attempt(Some(user_id), "10.0.0.1", false, None, None) .await; monitor .record_login_attempt(Some(user_id), "10.0.0.1", false, None, None) .await; assert!(monitor.is_locked(&identifier).await); monitor.unlock(&identifier).await; assert!(!monitor.is_locked(&identifier).await); } #[tokio::test] async fn test_ip_blocking() { let monitor = SecurityMonitor::with_defaults(); monitor .block_ip("1.2.3.4", Duration::minutes(30), "Test block") .await; assert!(monitor.is_ip_blocked("1.2.3.4").await); assert!(!monitor.is_ip_blocked("5.6.7.8").await); monitor.unblock_ip("1.2.3.4").await; assert!(!monitor.is_ip_blocked("1.2.3.4").await); } #[test] fn test_security_event_creation() { let event = SecurityEvent::new(SecurityEventType::LoginFailure) .with_user(Uuid::new_v4()) .with_ip("192.168.1.1".into()) .with_detail("reason", serde_json::json!("invalid_password")); assert_eq!(event.event_type, SecurityEventType::LoginFailure); assert!(event.user_id.is_some()); assert!(event.ip_address.is_some()); } #[test] fn test_alert_creation() { let mut alert = SecurityAlert::new( AlertSeverity::High, "Test Alert", "Test description", ); assert!(!alert.acknowledged); assert!(!alert.resolved); alert.acknowledge(Uuid::new_v4()); assert!(alert.acknowledged); alert.resolve(); assert!(alert.resolved); } #[test] fn test_geo_location_distance() { let loc1 = GeoLocation::new() .with_coordinates(40.7128, -74.0060); let loc2 = GeoLocation::new() .with_coordinates(51.5074, -0.1278); let distance = loc1.distance_km(&loc2).unwrap(); assert!(distance > 5500.0 && distance < 5600.0); } #[test] fn test_haversine_distance() { let distance = haversine_distance(40.7128, -74.0060, 51.5074, -0.1278); assert!(distance > 5500.0 && distance < 5600.0); } #[test] fn test_user_security_profile() { let user_id = Uuid::new_v4(); let mut profile = UserSecurityProfile::new(user_id); assert!(!profile.is_known_ip("192.168.1.1")); profile.add_known_ip("192.168.1.1"); assert!(profile.is_known_ip("192.168.1.1")); profile.lock(Duration::minutes(30)); assert!(profile.check_lock_status()); profile.unlock(); assert!(!profile.check_lock_status()); } #[test] fn test_lockout_record() { let lockout = LockoutRecord { identifier: "test".into(), locked_at: Utc::now(), expires_at: Utc::now() + Duration::minutes(30), reason: "Test".into(), attempt_count: 5, }; assert!(!lockout.is_expired()); assert!(lockout.remaining_time() > Duration::zero()); } #[test] fn test_event_severity_mapping() { assert_eq!( SecurityEventType::BruteForceDetected.severity(), AlertSeverity::Critical ); assert_eq!( SecurityEventType::LoginFailure.severity(), AlertSeverity::High ); assert_eq!( SecurityEventType::LoginSuccess.severity(), AlertSeverity::Low ); } #[tokio::test] async fn test_alert_acknowledgment() { let monitor = SecurityMonitor::with_defaults(); let event = SecurityEvent::new(SecurityEventType::BruteForceDetected); monitor.record_event(event).await; let alerts = monitor.get_alerts(true, 10).await; assert!(!alerts.is_empty()); let alert_id = alerts[0].id; let admin_id = Uuid::new_v4(); assert!(monitor.acknowledge_alert(alert_id, admin_id).await); let unack_alerts = monitor.get_alerts(true, 10).await; assert!(unack_alerts.iter().all(|a| a.id != alert_id)); } #[tokio::test] async fn test_cleanup() { let mut config = SecurityMonitoringConfig::default(); config.retention_hours = 0; let monitor = SecurityMonitor::new(config); let event = SecurityEvent::new(SecurityEventType::LoginSuccess); monitor.record_event(event).await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let cleaned = monitor.cleanup_old_data().await; assert!(cleaned >= 1); } #[test] fn test_alert_severity_score() { assert_eq!(AlertSeverity::Low.score(), 25); assert_eq!(AlertSeverity::Medium.score(), 50); assert_eq!(AlertSeverity::High.score(), 75); assert_eq!(AlertSeverity::Critical.score(), 100); } }