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