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 parking_lot::RwLock;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::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
167    user_profiles: Arc<RwLock<HashMap<String, UserProfile>>>,
168    tenant_profiles: Arc<RwLock<HashMap<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(RwLock::new(HashMap::new())),
180            tenant_profiles: Arc::new(RwLock::new(HashMap::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            if let Some((score, factors)) = self.detect_brute_force(&user_id, event)? {
225                anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
226            }
227        }
228
229        // Check for unusual access patterns
230        if config.enable_unusual_access_detection {
231            if let Some((score, factors)) = self.detect_unusual_access(&user_id, event)? {
232                anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
233            }
234        }
235
236        // Check for privilege escalation
237        if config.enable_privilege_escalation_detection {
238            if let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)? {
239                anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
240            }
241        }
242
243        // Check for data exfiltration
244        if config.enable_data_exfiltration_detection {
245            if let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)? {
246                anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
247            }
248        }
249
250        // Check for velocity anomalies
251        if config.enable_velocity_detection {
252            if let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)? {
253                anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
254            }
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            _ => return Ok(()),
318        };
319
320        let mut profiles = self.user_profiles.write();
321        let profile = profiles
322            .entry(format!("{}-{}", event.tenant_id().as_str(), user_id))
323            .or_insert_with(|| {
324                UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
325            });
326
327        // Update activity patterns
328        let hour = event.timestamp().hour();
329        if !profile.typical_hours.contains(&hour) {
330            profile.typical_hours.push(hour);
331        }
332
333        // Update action frequency
334        *profile
335            .typical_actions
336            .entry(event.action().clone())
337            .or_insert(0) += 1;
338
339        // Update statistics
340        profile.event_count += 1;
341        profile.last_updated = Utc::now();
342
343        // Recalculate averages (simple moving average)
344        let events_in_window = profile.event_count.min(1000);
345        profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
346
347        Ok(())
348    }
349
350    // === Detection Methods ===
351
352    fn detect_brute_force(
353        &self,
354        user_id: &str,
355        event: &AuditEvent,
356    ) -> Result<Option<(f64, Vec<String>)>> {
357        // Detect multiple failed login attempts
358        if event.action() != &AuditAction::Login {
359            return Ok(None);
360        }
361
362        let recent = self.recent_events.read();
363        let mut recent_failures = recent
364            .iter()
365            .filter(|e| {
366                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
367                    uid == user_id
368                        && e.action() == &AuditAction::Login
369                        && e.outcome() == &AuditOutcome::Failure
370                        && (Utc::now() - e.timestamp()) < Duration::minutes(15)
371                } else {
372                    false
373                }
374            })
375            .count();
376
377        // Include current event if it's also a failure
378        if event.outcome() == &AuditOutcome::Failure {
379            recent_failures += 1;
380        }
381
382        if recent_failures >= 5 {
383            let score = (recent_failures as f64 / 10.0).min(1.0);
384            let factors = vec![format!(
385                "{} failed login attempts in 15 minutes",
386                recent_failures
387            )];
388            return Ok(Some((score, factors)));
389        }
390
391        Ok(None)
392    }
393
394    fn detect_unusual_access(
395        &self,
396        user_id: &str,
397        event: &AuditEvent,
398    ) -> Result<Option<(f64, Vec<String>)>> {
399        let profiles = self.user_profiles.read();
400        let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
401
402        if let Some(profile) = profiles.get(&profile_key) {
403            if profile.event_count < self.config.read().min_baseline_events {
404                return Ok(None); // Not enough data for baseline
405            }
406
407            let mut factors = Vec::new();
408            let mut anomaly_indicators = 0;
409
410            // Check if access is outside typical hours
411            let hour = event.timestamp().hour();
412            if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
413                factors.push(format!("Access at unusual hour: {}:00", hour));
414                anomaly_indicators += 1;
415            }
416
417            // Check if action is unusual for this user
418            let action_count = profile
419                .typical_actions
420                .get(event.action())
421                .copied()
422                .unwrap_or(0);
423            if action_count == 0 && profile.event_count > 50 {
424                factors.push(format!("First time performing {:?}", event.action()));
425                anomaly_indicators += 1;
426            }
427
428            if anomaly_indicators > 0 {
429                let score = (anomaly_indicators as f64 / 2.0).min(1.0);
430                return Ok(Some((score, factors)));
431            }
432        }
433
434        Ok(None)
435    }
436
437    fn detect_privilege_escalation(
438        &self,
439        user_id: &str,
440        event: &AuditEvent,
441    ) -> Result<Option<(f64, Vec<String>)>> {
442        // Detect attempts to gain unauthorized privileges
443        let sensitive_actions = vec![AuditAction::TenantUpdated, AuditAction::RoleChanged];
444
445        if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
446            let recent = self.recent_events.read();
447            let recent_privilege_attempts = recent
448                .iter()
449                .filter(|e| {
450                    if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
451                        uid == user_id
452                            && sensitive_actions.contains(e.action())
453                            && (Utc::now() - e.timestamp()) < Duration::hours(1)
454                    } else {
455                        false
456                    }
457                })
458                .count();
459
460            if recent_privilege_attempts >= 3 {
461                let score = 0.8;
462                let factors = vec![
463                    format!(
464                        "{} privilege escalation attempts in 1 hour",
465                        recent_privilege_attempts
466                    ),
467                    format!("Latest action: {:?}", event.action()),
468                ];
469                return Ok(Some((score, factors)));
470            }
471        }
472
473        Ok(None)
474    }
475
476    fn detect_data_exfiltration(
477        &self,
478        user_id: &str,
479        event: &AuditEvent,
480    ) -> Result<Option<(f64, Vec<String>)>> {
481        // Detect unusual data access patterns that might indicate exfiltration
482        if event.action() != &AuditAction::EventQueried {
483            return Ok(None);
484        }
485
486        let recent = self.recent_events.read();
487        let recent_queries = recent
488            .iter()
489            .filter(|e| {
490                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
491                    uid == user_id
492                        && e.action() == &AuditAction::EventQueried
493                        && (Utc::now() - e.timestamp()) < Duration::hours(1)
494                } else {
495                    false
496                }
497            })
498            .count();
499
500        // Check user profile for baseline
501        let profiles = self.user_profiles.read();
502        let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
503
504        if let Some(profile) = profiles.get(&profile_key) {
505            if profile.event_count >= self.config.read().min_baseline_events {
506                // If current query rate is 5x normal, flag as anomalous
507                if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
508                    let score = 0.75;
509                    let factors = vec![
510                        format!(
511                            "{} queries in 1 hour (baseline: {:.0})",
512                            recent_queries, profile.avg_actions_per_hour
513                        ),
514                        "Potential data exfiltration pattern".to_string(),
515                    ];
516                    return Ok(Some((score, factors)));
517                }
518            }
519        }
520
521        Ok(None)
522    }
523
524    fn detect_velocity_anomaly(
525        &self,
526        user_id: &str,
527        event: &AuditEvent,
528    ) -> Result<Option<(f64, Vec<String>)>> {
529        // Detect impossibly fast actions (e.g., actions from different locations in short time)
530        let recent = self.recent_events.read();
531        let very_recent = recent
532            .iter()
533            .filter(|e| {
534                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
535                    uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
536                } else {
537                    false
538                }
539            })
540            .count();
541
542        // More than 20 actions in 10 seconds is suspicious
543        if very_recent >= 20 {
544            let score = 0.7;
545            let factors = vec![
546                format!("{} actions in 10 seconds", very_recent),
547                "Potential automated attack or compromised credentials".to_string(),
548            ];
549            return Ok(Some((score, factors)));
550        }
551
552        Ok(None)
553    }
554
555    pub fn add_recent_event(&self, event: AuditEvent) {
556        let mut events = self.recent_events.write();
557        events.push(event);
558
559        // Keep only recent events (last 24 hours)
560        let cutoff = Utc::now() - Duration::hours(24);
561        events.retain(|e| e.timestamp() > &cutoff);
562
563        // Limit size to prevent memory issues
564        if events.len() > 10000 {
565            events.drain(0..1000);
566        }
567    }
568
569    /// Get statistics about detection
570    pub fn get_stats(&self) -> DetectionStats {
571        let profiles = self.user_profiles.read();
572        let recent = self.recent_events.read();
573
574        DetectionStats {
575            user_profiles_count: 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    #[test]
605    fn test_anomaly_detector_creation() {
606        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
607        let stats = detector.get_stats();
608        assert_eq!(stats.user_profiles_count, 0);
609        assert_eq!(stats.recent_events_count, 0);
610    }
611
612    #[test]
613    fn test_normal_behavior_not_flagged() {
614        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
615        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
616
617        let result = detector.analyze_event(&event).unwrap();
618        assert!(!result.is_anomalous);
619        assert_eq!(result.recommended_action, RecommendedAction::Monitor);
620    }
621
622    #[test]
623    fn test_brute_force_detection() {
624        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
625
626        // Simulate 6 failed login attempts
627        for _ in 0..6 {
628            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
629            detector.add_recent_event(event.clone());
630        }
631
632        // Next login attempt should be flagged
633        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
634        let result = detector.analyze_event(&event).unwrap();
635
636        assert!(result.is_anomalous);
637        assert_eq!(result.anomaly_type, Some(AnomalyType::BruteForceAttack));
638        assert!(result.score >= 0.5);
639    }
640
641    #[test]
642    fn test_profile_building() {
643        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
644        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
645
646        detector.update_profile(&event).unwrap();
647
648        let stats = detector.get_stats();
649        assert_eq!(stats.user_profiles_count, 1);
650    }
651
652    #[test]
653    fn test_velocity_anomaly() {
654        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
655
656        // Simulate 25 actions in rapid succession
657        for _ in 0..25 {
658            let event =
659                create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
660            detector.add_recent_event(event.clone());
661        }
662
663        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
664        let result = detector.analyze_event(&event).unwrap();
665
666        assert!(result.is_anomalous);
667        assert_eq!(result.anomaly_type, Some(AnomalyType::VelocityAnomaly));
668    }
669
670    #[test]
671    fn test_disabled_detection() {
672        let mut config = AnomalyDetectionConfig::default();
673        config.enabled = false;
674
675        let detector = AnomalyDetector::new(config);
676        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
677
678        let result = detector.analyze_event(&event).unwrap();
679        assert!(!result.is_anomalous);
680    }
681}