Skip to main content

allsource_core/security/
anomaly_detection.rs

1/// ML-Based Anomaly Detection System
2///
3/// Detects suspicious patterns in audit logs using statistical analysis
4/// and machine learning techniques to identify:
5/// - Unusual access patterns
6/// - Brute force attacks
7/// - Privilege escalation attempts
8/// - Data exfiltration patterns
9/// - Account compromise indicators
10use crate::domain::entities::{AuditAction, AuditEvent, AuditOutcome};
11use crate::error::Result;
12use chrono::{DateTime, Duration, Timelike, Utc};
13use dashmap::DashMap;
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use std::{collections::HashMap, sync::Arc};
17
18/// Anomaly detection configuration
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AnomalyDetectionConfig {
21    /// Enable anomaly detection
22    pub enabled: bool,
23
24    /// Sensitivity level (0.0 = very lenient, 1.0 = very strict)
25    pub sensitivity: f64,
26
27    /// Minimum events required for baseline
28    pub min_baseline_events: usize,
29
30    /// Time window for analysis (hours)
31    pub analysis_window_hours: i64,
32
33    /// Enable specific detectors
34    pub enable_brute_force_detection: bool,
35    pub enable_unusual_access_detection: bool,
36    pub enable_privilege_escalation_detection: bool,
37    pub enable_data_exfiltration_detection: bool,
38    pub enable_velocity_detection: bool,
39}
40
41impl Default for AnomalyDetectionConfig {
42    fn default() -> Self {
43        Self {
44            enabled: true,
45            sensitivity: 0.7,
46            min_baseline_events: 100,
47            analysis_window_hours: 24,
48            enable_brute_force_detection: true,
49            enable_unusual_access_detection: true,
50            enable_privilege_escalation_detection: true,
51            enable_data_exfiltration_detection: true,
52            enable_velocity_detection: true,
53        }
54    }
55}
56
57/// Anomaly detection result
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AnomalyResult {
60    /// Is this event anomalous?
61    pub is_anomalous: bool,
62
63    /// Anomaly score (0.0 = normal, 1.0 = highly anomalous)
64    pub score: f64,
65
66    /// Type of anomaly detected
67    pub anomaly_type: Option<AnomalyType>,
68
69    /// Detailed reason
70    pub reason: String,
71
72    /// Recommended action
73    pub recommended_action: RecommendedAction,
74
75    /// Contributing factors
76    pub factors: Vec<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80pub enum AnomalyType {
81    BruteForceAttack,
82    UnusualAccessPattern,
83    PrivilegeEscalation,
84    DataExfiltration,
85    VelocityAnomaly,
86    AccountCompromise,
87    SuspiciousActivity,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub enum RecommendedAction {
92    Monitor,      // Continue monitoring
93    Alert,        // Send alert to security team
94    Block,        // Block the action
95    RequireMFA,   // Require additional authentication
96    RevokeAccess, // Immediately revoke access
97}
98
99/// User behavior profile for baseline comparison
100#[derive(Debug, Clone, Serialize, Deserialize)]
101struct UserProfile {
102    user_id: String,
103    tenant_id: String,
104
105    // Activity patterns
106    typical_hours: Vec<u32>, // Hours of day when user is typically active
107    typical_actions: HashMap<AuditAction, usize>, // Action frequency
108    typical_locations: Vec<String>, // IP addresses or locations
109
110    // Statistical baselines
111    avg_actions_per_hour: f64,
112    avg_actions_per_day: f64,
113    max_actions_per_hour: usize,
114
115    // Failure rates
116    avg_failure_rate: f64,
117
118    // Last updated
119    last_updated: DateTime<Utc>,
120    event_count: usize,
121}
122
123impl UserProfile {
124    fn new(user_id: String, tenant_id: String) -> Self {
125        Self {
126            user_id,
127            tenant_id,
128            typical_hours: Vec::new(),
129            typical_actions: HashMap::new(),
130            typical_locations: Vec::new(),
131            avg_actions_per_hour: 0.0,
132            avg_actions_per_day: 0.0,
133            max_actions_per_hour: 0,
134            avg_failure_rate: 0.0,
135            last_updated: Utc::now(),
136            event_count: 0,
137        }
138    }
139}
140
141/// Tenant behavior profile for organizational patterns
142#[derive(Debug, Clone, Serialize, Deserialize)]
143struct TenantProfile {
144    tenant_id: String,
145
146    // Access patterns
147    typical_daily_events: f64,
148    typical_hourly_events: f64,
149    peak_hours: Vec<u32>,
150
151    // User activity
152    active_users_per_day: f64,
153
154    // Security metrics
155    avg_failure_rate: f64,
156    suspicious_event_rate: f64,
157
158    last_updated: DateTime<Utc>,
159    event_count: usize,
160}
161
162/// ML-based anomaly detector
163pub struct AnomalyDetector {
164    config: Arc<RwLock<AnomalyDetectionConfig>>,
165
166    // Behavior profiles - using DashMap for lock-free concurrent access
167    user_profiles: Arc<DashMap<String, UserProfile>>,
168    tenant_profiles: Arc<DashMap<String, TenantProfile>>,
169
170    // Recent events for pattern analysis
171    recent_events: Arc<RwLock<Vec<AuditEvent>>>,
172}
173
174impl AnomalyDetector {
175    /// Create new anomaly detector
176    pub fn new(config: AnomalyDetectionConfig) -> Self {
177        Self {
178            config: Arc::new(RwLock::new(config)),
179            user_profiles: Arc::new(DashMap::new()),
180            tenant_profiles: Arc::new(DashMap::new()),
181            recent_events: Arc::new(RwLock::new(Vec::new())),
182        }
183    }
184
185    /// Analyze an audit event for anomalies
186    pub fn analyze_event(&self, event: &AuditEvent) -> Result<AnomalyResult> {
187        let config = self.config.read();
188
189        if !config.enabled {
190            return Ok(AnomalyResult {
191                is_anomalous: false,
192                score: 0.0,
193                anomaly_type: None,
194                reason: "Anomaly detection disabled".to_string(),
195                recommended_action: RecommendedAction::Monitor,
196                factors: vec![],
197            });
198        }
199
200        let mut anomaly_scores: Vec<(AnomalyType, f64, Vec<String>)> = Vec::new();
201
202        // Get user ID from actor
203        let user_id = match event.actor() {
204            crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
205            crate::domain::entities::Actor::System { .. } => {
206                // System actions are typically not anomalous
207                return Ok(AnomalyResult {
208                    is_anomalous: false,
209                    score: 0.0,
210                    anomaly_type: None,
211                    reason: "System actor".to_string(),
212                    recommended_action: RecommendedAction::Monitor,
213                    factors: vec![],
214                });
215            }
216            crate::domain::entities::Actor::ApiKey {
217                key_id,
218                key_name: _,
219            } => key_id.clone(),
220        };
221
222        // Check for brute force attacks
223        if config.enable_brute_force_detection
224            && let Some((score, factors)) = self.detect_brute_force(&user_id, event)?
225        {
226            anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
227        }
228
229        // Check for unusual access patterns
230        if config.enable_unusual_access_detection
231            && let Some((score, factors)) = self.detect_unusual_access(&user_id, event)?
232        {
233            anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
234        }
235
236        // Check for privilege escalation
237        if config.enable_privilege_escalation_detection
238            && let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)?
239        {
240            anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
241        }
242
243        // Check for data exfiltration
244        if config.enable_data_exfiltration_detection
245            && let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)?
246        {
247            anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
248        }
249
250        // Check for velocity anomalies
251        if config.enable_velocity_detection
252            && let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)?
253        {
254            anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
255        }
256
257        // Store event for future analysis
258        self.add_recent_event(event.clone());
259
260        // Calculate overall anomaly score (max of all detectors)
261        let (max_anomaly_type, max_score, all_factors) = if anomaly_scores.is_empty() {
262            (None, 0.0, vec![])
263        } else {
264            let max_entry = anomaly_scores
265                .iter()
266                .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
267                .unwrap();
268            let all_factors: Vec<String> = anomaly_scores
269                .iter()
270                .flat_map(|(_, _, f)| f.clone())
271                .collect();
272            (Some(max_entry.0.clone()), max_entry.1, all_factors)
273        };
274
275        let is_anomalous = max_score >= config.sensitivity;
276
277        let recommended_action = if max_score >= 0.9 {
278            RecommendedAction::RevokeAccess
279        } else if max_score >= 0.8 {
280            RecommendedAction::Block
281        } else if max_score >= 0.7 {
282            RecommendedAction::RequireMFA
283        } else if max_score >= 0.5 {
284            RecommendedAction::Alert
285        } else {
286            RecommendedAction::Monitor
287        };
288
289        let reason = if is_anomalous {
290            format!(
291                "Anomalous {:?} detected with score {:.2}",
292                max_anomaly_type.as_ref().unwrap(),
293                max_score
294            )
295        } else {
296            "Normal behavior".to_string()
297        };
298
299        Ok(AnomalyResult {
300            is_anomalous,
301            score: max_score,
302            anomaly_type: max_anomaly_type,
303            reason,
304            recommended_action,
305            factors: all_factors,
306        })
307    }
308
309    /// Update user profile with new event
310    pub fn update_profile(&self, event: &AuditEvent) -> Result<()> {
311        let user_id = match event.actor() {
312            crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
313            crate::domain::entities::Actor::ApiKey {
314                key_id,
315                key_name: _,
316            } => key_id.clone(),
317            crate::domain::entities::Actor::System { .. } => return Ok(()),
318        };
319
320        let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
321        let mut profile = self.user_profiles.entry(profile_key).or_insert_with(|| {
322            UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
323        });
324
325        // Update activity patterns
326        let hour = event.timestamp().hour();
327        if !profile.typical_hours.contains(&hour) {
328            profile.typical_hours.push(hour);
329        }
330
331        // Update action frequency
332        *profile
333            .typical_actions
334            .entry(event.action().clone())
335            .or_insert(0) += 1;
336
337        // Update statistics
338        profile.event_count += 1;
339        profile.last_updated = Utc::now();
340
341        // Recalculate averages (simple moving average)
342        let events_in_window = profile.event_count.min(1000);
343        profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
344
345        Ok(())
346    }
347
348    // === Detection Methods ===
349
350    fn detect_brute_force(
351        &self,
352        user_id: &str,
353        event: &AuditEvent,
354    ) -> Result<Option<(f64, Vec<String>)>> {
355        // Detect multiple failed login attempts
356        if event.action() != &AuditAction::Login {
357            return Ok(None);
358        }
359
360        let recent = self.recent_events.read();
361        let mut recent_failures = recent
362            .iter()
363            .filter(|e| {
364                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
365                    uid == user_id
366                        && e.action() == &AuditAction::Login
367                        && e.outcome() == &AuditOutcome::Failure
368                        && (Utc::now() - e.timestamp()) < Duration::minutes(15)
369                } else {
370                    false
371                }
372            })
373            .count();
374
375        // Include current event if it's also a failure
376        if event.outcome() == &AuditOutcome::Failure {
377            recent_failures += 1;
378        }
379
380        if recent_failures >= 5 {
381            let score = (recent_failures as f64 / 10.0).min(1.0);
382            let factors = vec![format!(
383                "{} failed login attempts in 15 minutes",
384                recent_failures
385            )];
386            return Ok(Some((score, factors)));
387        }
388
389        Ok(None)
390    }
391
392    fn detect_unusual_access(
393        &self,
394        user_id: &str,
395        event: &AuditEvent,
396    ) -> Result<Option<(f64, Vec<String>)>> {
397        let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
398
399        if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
400            let profile = profile_ref.value();
401            if profile.event_count < self.config.read().min_baseline_events {
402                return Ok(None); // Not enough data for baseline
403            }
404
405            let mut factors = Vec::new();
406            let mut anomaly_indicators = 0;
407
408            // Check if access is outside typical hours
409            let hour = event.timestamp().hour();
410            if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
411                factors.push(format!("Access at unusual hour: {hour}:00"));
412                anomaly_indicators += 1;
413            }
414
415            // Check if action is unusual for this user
416            let action_count = profile
417                .typical_actions
418                .get(event.action())
419                .copied()
420                .unwrap_or(0);
421            if action_count == 0 && profile.event_count > 50 {
422                factors.push(format!("First time performing {:?}", event.action()));
423                anomaly_indicators += 1;
424            }
425
426            if anomaly_indicators > 0 {
427                let score = (f64::from(anomaly_indicators) / 2.0).min(1.0);
428                return Ok(Some((score, factors)));
429            }
430        }
431
432        Ok(None)
433    }
434
435    fn detect_privilege_escalation(
436        &self,
437        user_id: &str,
438        event: &AuditEvent,
439    ) -> Result<Option<(f64, Vec<String>)>> {
440        // Detect attempts to gain unauthorized privileges
441        let sensitive_actions = [AuditAction::TenantUpdated, AuditAction::RoleChanged];
442
443        if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
444            let recent = self.recent_events.read();
445            let recent_privilege_attempts = recent
446                .iter()
447                .filter(|e| {
448                    if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
449                        uid == user_id
450                            && sensitive_actions.contains(e.action())
451                            && (Utc::now() - e.timestamp()) < Duration::hours(1)
452                    } else {
453                        false
454                    }
455                })
456                .count();
457
458            if recent_privilege_attempts >= 3 {
459                let score = 0.8;
460                let factors = vec![
461                    format!(
462                        "{} privilege escalation attempts in 1 hour",
463                        recent_privilege_attempts
464                    ),
465                    format!("Latest action: {:?}", event.action()),
466                ];
467                return Ok(Some((score, factors)));
468            }
469        }
470
471        Ok(None)
472    }
473
474    fn detect_data_exfiltration(
475        &self,
476        user_id: &str,
477        event: &AuditEvent,
478    ) -> Result<Option<(f64, Vec<String>)>> {
479        // Detect unusual data access patterns that might indicate exfiltration
480        if event.action() != &AuditAction::EventQueried {
481            return Ok(None);
482        }
483
484        let recent = self.recent_events.read();
485        let recent_queries = recent
486            .iter()
487            .filter(|e| {
488                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
489                    uid == user_id
490                        && e.action() == &AuditAction::EventQueried
491                        && (Utc::now() - e.timestamp()) < Duration::hours(1)
492                } else {
493                    false
494                }
495            })
496            .count();
497
498        // Check user profile for baseline
499        let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
500
501        if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
502            let profile = profile_ref.value();
503            if profile.event_count >= self.config.read().min_baseline_events {
504                // If current query rate is 5x normal, flag as anomalous
505                if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
506                    let score = 0.75;
507                    let factors = vec![
508                        format!(
509                            "{} queries in 1 hour (baseline: {:.0})",
510                            recent_queries, profile.avg_actions_per_hour
511                        ),
512                        "Potential data exfiltration pattern".to_string(),
513                    ];
514                    return Ok(Some((score, factors)));
515                }
516            }
517        }
518
519        Ok(None)
520    }
521
522    fn detect_velocity_anomaly(
523        &self,
524        user_id: &str,
525        event: &AuditEvent,
526    ) -> Result<Option<(f64, Vec<String>)>> {
527        // Detect impossibly fast actions (e.g., actions from different locations in short time)
528        let recent = self.recent_events.read();
529        let very_recent = recent
530            .iter()
531            .filter(|e| {
532                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
533                    uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
534                } else {
535                    false
536                }
537            })
538            .count();
539
540        // More than 20 actions in 10 seconds is suspicious
541        if very_recent >= 20 {
542            let score = 0.7;
543            let factors = vec![
544                format!("{very_recent} actions in 10 seconds"),
545                "Potential automated attack or compromised credentials".to_string(),
546            ];
547            return Ok(Some((score, factors)));
548        }
549
550        Ok(None)
551    }
552
553    pub fn add_recent_event(&self, event: AuditEvent) {
554        let mut events = self.recent_events.write();
555        events.push(event);
556
557        // Keep only recent events (last 24 hours)
558        let cutoff = Utc::now() - Duration::hours(24);
559        events.retain(|e| e.timestamp() > &cutoff);
560
561        // Limit size to prevent memory issues
562        if events.len() > 10000 {
563            events.drain(0..1000);
564        }
565    }
566
567    /// Get statistics about detection
568    pub fn get_stats(&self) -> DetectionStats {
569        let recent = self.recent_events.read();
570
571        DetectionStats {
572            user_profiles_count: self.user_profiles.len(),
573            recent_events_count: recent.len(),
574            config: self.config.read().clone(),
575        }
576    }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct DetectionStats {
581    pub user_profiles_count: usize,
582    pub recent_events_count: usize,
583    pub config: AnomalyDetectionConfig,
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use crate::domain::{entities::Actor, value_objects::TenantId};
590
591    fn create_test_event(action: AuditAction, outcome: AuditOutcome, user_id: &str) -> AuditEvent {
592        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
593        let actor = Actor::User {
594            user_id: user_id.to_string(),
595            username: "testuser".to_string(),
596        };
597        AuditEvent::new(tenant_id, action, actor, outcome)
598    }
599
600    fn create_system_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
601        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
602        let actor = Actor::System {
603            component: "test-service".to_string(),
604        };
605        AuditEvent::new(tenant_id, action, actor, outcome)
606    }
607
608    fn create_api_key_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
609        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
610        let actor = Actor::ApiKey {
611            key_id: "key-123".to_string(),
612            key_name: "test-key".to_string(),
613        };
614        AuditEvent::new(tenant_id, action, actor, outcome)
615    }
616
617    #[test]
618    fn test_anomaly_detector_creation() {
619        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
620        let stats = detector.get_stats();
621        assert_eq!(stats.user_profiles_count, 0);
622        assert_eq!(stats.recent_events_count, 0);
623    }
624
625    #[test]
626    fn test_normal_behavior_not_flagged() {
627        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
628        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
629
630        let result = detector.analyze_event(&event).unwrap();
631        assert!(!result.is_anomalous);
632        assert_eq!(result.recommended_action, RecommendedAction::Monitor);
633    }
634
635    #[test]
636    fn test_brute_force_detection() {
637        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
638
639        // Simulate 6 failed login attempts
640        for _ in 0..6 {
641            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
642            detector.add_recent_event(event.clone());
643        }
644
645        // Next login attempt should be flagged
646        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
647        let result = detector.analyze_event(&event).unwrap();
648
649        assert!(result.is_anomalous);
650        assert_eq!(result.anomaly_type, Some(AnomalyType::BruteForceAttack));
651        assert!(result.score >= 0.5);
652    }
653
654    #[test]
655    fn test_profile_building() {
656        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
657        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
658
659        detector.update_profile(&event).unwrap();
660
661        let stats = detector.get_stats();
662        assert_eq!(stats.user_profiles_count, 1);
663    }
664
665    #[test]
666    fn test_velocity_anomaly() {
667        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
668
669        // Simulate 25 actions in rapid succession
670        for _ in 0..25 {
671            let event =
672                create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
673            detector.add_recent_event(event.clone());
674        }
675
676        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
677        let result = detector.analyze_event(&event).unwrap();
678
679        assert!(result.is_anomalous);
680        assert_eq!(result.anomaly_type, Some(AnomalyType::VelocityAnomaly));
681    }
682
683    #[test]
684    fn test_disabled_detection() {
685        let config = AnomalyDetectionConfig {
686            enabled: false,
687            ..Default::default()
688        };
689
690        let detector = AnomalyDetector::new(config);
691        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
692
693        let result = detector.analyze_event(&event).unwrap();
694        assert!(!result.is_anomalous);
695    }
696
697    #[test]
698    fn test_default_config() {
699        let config = AnomalyDetectionConfig::default();
700        assert!(config.enabled);
701        assert_eq!(config.sensitivity, 0.7);
702        assert_eq!(config.min_baseline_events, 100);
703        assert_eq!(config.analysis_window_hours, 24);
704        assert!(config.enable_brute_force_detection);
705        assert!(config.enable_unusual_access_detection);
706        assert!(config.enable_privilege_escalation_detection);
707        assert!(config.enable_data_exfiltration_detection);
708        assert!(config.enable_velocity_detection);
709    }
710
711    #[test]
712    fn test_config_serde() {
713        let config = AnomalyDetectionConfig::default();
714        let json = serde_json::to_string(&config).unwrap();
715        let parsed: AnomalyDetectionConfig = serde_json::from_str(&json).unwrap();
716        assert_eq!(parsed.enabled, config.enabled);
717        assert_eq!(parsed.sensitivity, config.sensitivity);
718    }
719
720    #[test]
721    fn test_anomaly_type_equality() {
722        assert_eq!(AnomalyType::BruteForceAttack, AnomalyType::BruteForceAttack);
723        assert_ne!(AnomalyType::BruteForceAttack, AnomalyType::DataExfiltration);
724    }
725
726    #[test]
727    fn test_recommended_action_equality() {
728        assert_eq!(RecommendedAction::Monitor, RecommendedAction::Monitor);
729        assert_ne!(RecommendedAction::Monitor, RecommendedAction::Block);
730    }
731
732    #[test]
733    fn test_system_actor_not_flagged() {
734        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
735        let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
736
737        let result = detector.analyze_event(&event).unwrap();
738        assert!(!result.is_anomalous);
739        assert_eq!(result.reason, "System actor");
740    }
741
742    #[test]
743    fn test_api_key_actor_analyzed() {
744        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
745        let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
746
747        let result = detector.analyze_event(&event).unwrap();
748        assert!(!result.is_anomalous);
749    }
750
751    #[test]
752    fn test_update_profile_api_key() {
753        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
754        let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
755
756        detector.update_profile(&event).unwrap();
757
758        let stats = detector.get_stats();
759        assert_eq!(stats.user_profiles_count, 1);
760    }
761
762    #[test]
763    fn test_update_profile_system() {
764        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
765        let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
766
767        detector.update_profile(&event).unwrap();
768
769        let stats = detector.get_stats();
770        // System events don't create user profiles
771        assert_eq!(stats.user_profiles_count, 0);
772    }
773
774    #[test]
775    fn test_recent_events_pruning() {
776        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
777
778        // Add more than 10000 events
779        for i in 0..10050 {
780            let event = create_test_event(
781                AuditAction::EventQueried,
782                AuditOutcome::Success,
783                &format!("user{}", i % 100),
784            );
785            detector.add_recent_event(event);
786        }
787
788        let stats = detector.get_stats();
789        // Should be pruned to around 10000
790        assert!(stats.recent_events_count <= 10050);
791    }
792
793    #[test]
794    fn test_detection_stats_serde() {
795        let stats = DetectionStats {
796            user_profiles_count: 10,
797            recent_events_count: 100,
798            config: AnomalyDetectionConfig::default(),
799        };
800
801        let json = serde_json::to_string(&stats).unwrap();
802        assert!(json.contains("\"user_profiles_count\":10"));
803        assert!(json.contains("\"recent_events_count\":100"));
804    }
805
806    #[test]
807    fn test_anomaly_result_serde() {
808        let result = AnomalyResult {
809            is_anomalous: true,
810            score: 0.85,
811            anomaly_type: Some(AnomalyType::BruteForceAttack),
812            reason: "Test reason".to_string(),
813            recommended_action: RecommendedAction::Block,
814            factors: vec!["factor1".to_string(), "factor2".to_string()],
815        };
816
817        let json = serde_json::to_string(&result).unwrap();
818        let parsed: AnomalyResult = serde_json::from_str(&json).unwrap();
819        assert_eq!(parsed.is_anomalous, result.is_anomalous);
820        assert_eq!(parsed.score, result.score);
821        assert_eq!(parsed.anomaly_type, result.anomaly_type);
822    }
823
824    #[test]
825    fn test_recommended_action_for_high_score() {
826        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
827
828        // Simulate many failed login attempts to get high score
829        for _ in 0..15 {
830            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
831            detector.add_recent_event(event.clone());
832        }
833
834        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
835        let result = detector.analyze_event(&event).unwrap();
836
837        assert!(result.is_anomalous);
838        // With score >= 0.9, should recommend RevokeAccess
839        if result.score >= 0.9 {
840            assert_eq!(result.recommended_action, RecommendedAction::RevokeAccess);
841        } else if result.score >= 0.8 {
842            assert_eq!(result.recommended_action, RecommendedAction::Block);
843        }
844    }
845
846    #[test]
847    fn test_successful_login_after_failures() {
848        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
849
850        // Add some failed logins
851        for _ in 0..3 {
852            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
853            detector.add_recent_event(event);
854        }
855
856        // Successful login should not trigger brute force (under threshold)
857        let event = create_test_event(AuditAction::Login, AuditOutcome::Success, "user1");
858        let result = detector.analyze_event(&event).unwrap();
859
860        // With only 3 failures, should not be flagged (threshold is 5)
861        assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
862    }
863
864    #[test]
865    fn test_privilege_escalation_detection() {
866        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
867
868        // Simulate multiple failed privilege escalation attempts
869        for _ in 0..4 {
870            let event =
871                create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
872            detector.add_recent_event(event);
873        }
874
875        let event = create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
876        let result = detector.analyze_event(&event).unwrap();
877
878        assert!(result.is_anomalous);
879        assert_eq!(result.anomaly_type, Some(AnomalyType::PrivilegeEscalation));
880    }
881
882    #[test]
883    fn test_non_login_action_not_brute_force() {
884        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
885
886        // Simulate many failed query attempts (not login)
887        for _ in 0..10 {
888            let event =
889                create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
890            detector.add_recent_event(event);
891        }
892
893        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
894        let result = detector.analyze_event(&event).unwrap();
895
896        // Should not be flagged as brute force (only login events trigger this)
897        assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
898    }
899
900    #[test]
901    fn test_multiple_users_independent() {
902        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
903
904        // Simulate failed logins from user1
905        for _ in 0..6 {
906            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
907            detector.add_recent_event(event);
908        }
909
910        // Check user2's login - should not be flagged
911        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user2");
912        let result = detector.analyze_event(&event).unwrap();
913
914        // User2's single failure should not trigger brute force
915        assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
916    }
917
918    #[test]
919    fn test_anomaly_types_serde() {
920        let types = vec![
921            AnomalyType::BruteForceAttack,
922            AnomalyType::UnusualAccessPattern,
923            AnomalyType::PrivilegeEscalation,
924            AnomalyType::DataExfiltration,
925            AnomalyType::VelocityAnomaly,
926            AnomalyType::AccountCompromise,
927            AnomalyType::SuspiciousActivity,
928        ];
929
930        for anomaly_type in types {
931            let json = serde_json::to_string(&anomaly_type).unwrap();
932            let parsed: AnomalyType = serde_json::from_str(&json).unwrap();
933            assert_eq!(parsed, anomaly_type);
934        }
935    }
936
937    #[test]
938    fn test_recommended_actions_serde() {
939        let actions = vec![
940            RecommendedAction::Monitor,
941            RecommendedAction::Alert,
942            RecommendedAction::Block,
943            RecommendedAction::RequireMFA,
944            RecommendedAction::RevokeAccess,
945        ];
946
947        for action in actions {
948            let json = serde_json::to_string(&action).unwrap();
949            let parsed: RecommendedAction = serde_json::from_str(&json).unwrap();
950            assert_eq!(parsed, action);
951        }
952    }
953
954    #[test]
955    fn test_disabled_individual_detectors() {
956        let config = AnomalyDetectionConfig {
957            enabled: true,
958            enable_brute_force_detection: false,
959            enable_velocity_detection: false,
960            ..Default::default()
961        };
962
963        let detector = AnomalyDetector::new(config);
964
965        // Simulate conditions that would trigger brute force
966        for _ in 0..10 {
967            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
968            detector.add_recent_event(event);
969        }
970
971        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
972        let result = detector.analyze_event(&event).unwrap();
973
974        // Brute force detection is disabled, so should not be flagged as brute force
975        assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
976    }
977}