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
10
11use crate::domain::entities::{AuditEvent, AuditAction, AuditOutcome};
12use crate::domain::value_objects::TenantId;
13use crate::error::Result;
14use chrono::{DateTime, Duration, Timelike, Utc};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use parking_lot::RwLock;
19
20/// Anomaly detection configuration
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AnomalyDetectionConfig {
23    /// Enable anomaly detection
24    pub enabled: bool,
25
26    /// Sensitivity level (0.0 = very lenient, 1.0 = very strict)
27    pub sensitivity: f64,
28
29    /// Minimum events required for baseline
30    pub min_baseline_events: usize,
31
32    /// Time window for analysis (hours)
33    pub analysis_window_hours: i64,
34
35    /// Enable specific detectors
36    pub enable_brute_force_detection: bool,
37    pub enable_unusual_access_detection: bool,
38    pub enable_privilege_escalation_detection: bool,
39    pub enable_data_exfiltration_detection: bool,
40    pub enable_velocity_detection: bool,
41}
42
43impl Default for AnomalyDetectionConfig {
44    fn default() -> Self {
45        Self {
46            enabled: true,
47            sensitivity: 0.7,
48            min_baseline_events: 100,
49            analysis_window_hours: 24,
50            enable_brute_force_detection: true,
51            enable_unusual_access_detection: true,
52            enable_privilege_escalation_detection: true,
53            enable_data_exfiltration_detection: true,
54            enable_velocity_detection: true,
55        }
56    }
57}
58
59/// Anomaly detection result
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AnomalyResult {
62    /// Is this event anomalous?
63    pub is_anomalous: bool,
64
65    /// Anomaly score (0.0 = normal, 1.0 = highly anomalous)
66    pub score: f64,
67
68    /// Type of anomaly detected
69    pub anomaly_type: Option<AnomalyType>,
70
71    /// Detailed reason
72    pub reason: String,
73
74    /// Recommended action
75    pub recommended_action: RecommendedAction,
76
77    /// Contributing factors
78    pub factors: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82pub enum AnomalyType {
83    BruteForceAttack,
84    UnusualAccessPattern,
85    PrivilegeEscalation,
86    DataExfiltration,
87    VelocityAnomaly,
88    AccountCompromise,
89    SuspiciousActivity,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93pub enum RecommendedAction {
94    Monitor,          // Continue monitoring
95    Alert,            // Send alert to security team
96    Block,            // Block the action
97    RequireMFA,       // Require additional authentication
98    RevokeAccess,     // Immediately revoke access
99}
100
101/// User behavior profile for baseline comparison
102#[derive(Debug, Clone, Serialize, Deserialize)]
103struct UserProfile {
104    user_id: String,
105    tenant_id: String,
106
107    // Activity patterns
108    typical_hours: Vec<u32>,          // Hours of day when user is typically active
109    typical_actions: HashMap<AuditAction, usize>,  // Action frequency
110    typical_locations: Vec<String>,   // IP addresses or locations
111
112    // Statistical baselines
113    avg_actions_per_hour: f64,
114    avg_actions_per_day: f64,
115    max_actions_per_hour: usize,
116
117    // Failure rates
118    avg_failure_rate: f64,
119
120    // Last updated
121    last_updated: DateTime<Utc>,
122    event_count: usize,
123}
124
125impl UserProfile {
126    fn new(user_id: String, tenant_id: String) -> Self {
127        Self {
128            user_id,
129            tenant_id,
130            typical_hours: Vec::new(),
131            typical_actions: HashMap::new(),
132            typical_locations: Vec::new(),
133            avg_actions_per_hour: 0.0,
134            avg_actions_per_day: 0.0,
135            max_actions_per_hour: 0,
136            avg_failure_rate: 0.0,
137            last_updated: Utc::now(),
138            event_count: 0,
139        }
140    }
141}
142
143/// Tenant behavior profile for organizational patterns
144#[derive(Debug, Clone, Serialize, Deserialize)]
145struct TenantProfile {
146    tenant_id: String,
147
148    // Access patterns
149    typical_daily_events: f64,
150    typical_hourly_events: f64,
151    peak_hours: Vec<u32>,
152
153    // User activity
154    active_users_per_day: f64,
155
156    // Security metrics
157    avg_failure_rate: f64,
158    suspicious_event_rate: f64,
159
160    last_updated: DateTime<Utc>,
161    event_count: usize,
162}
163
164/// ML-based anomaly detector
165pub struct AnomalyDetector {
166    config: Arc<RwLock<AnomalyDetectionConfig>>,
167
168    // Behavior profiles
169    user_profiles: Arc<RwLock<HashMap<String, UserProfile>>>,
170    tenant_profiles: Arc<RwLock<HashMap<String, TenantProfile>>>,
171
172    // Recent events for pattern analysis
173    recent_events: Arc<RwLock<Vec<AuditEvent>>>,
174}
175
176impl AnomalyDetector {
177    /// Create new anomaly detector
178    pub fn new(config: AnomalyDetectionConfig) -> Self {
179        Self {
180            config: Arc::new(RwLock::new(config)),
181            user_profiles: Arc::new(RwLock::new(HashMap::new())),
182            tenant_profiles: Arc::new(RwLock::new(HashMap::new())),
183            recent_events: Arc::new(RwLock::new(Vec::new())),
184        }
185    }
186
187    /// Analyze an audit event for anomalies
188    pub fn analyze_event(&self, event: &AuditEvent) -> Result<AnomalyResult> {
189        let config = self.config.read();
190
191        if !config.enabled {
192            return Ok(AnomalyResult {
193                is_anomalous: false,
194                score: 0.0,
195                anomaly_type: None,
196                reason: "Anomaly detection disabled".to_string(),
197                recommended_action: RecommendedAction::Monitor,
198                factors: vec![],
199            });
200        }
201
202        let mut anomaly_scores: Vec<(AnomalyType, f64, Vec<String>)> = Vec::new();
203
204        // Get user ID from actor
205        let user_id = match event.actor() {
206            crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
207            crate::domain::entities::Actor::System { .. } => {
208                // System actions are typically not anomalous
209                return Ok(AnomalyResult {
210                    is_anomalous: false,
211                    score: 0.0,
212                    anomaly_type: None,
213                    reason: "System actor".to_string(),
214                    recommended_action: RecommendedAction::Monitor,
215                    factors: vec![],
216                });
217            }
218            crate::domain::entities::Actor::ApiKey { key_id, key_name: _ } => key_id.clone(),
219        };
220
221        // Check for brute force attacks
222        if config.enable_brute_force_detection {
223            if let Some((score, factors)) = self.detect_brute_force(&user_id, event)? {
224                anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
225            }
226        }
227
228        // Check for unusual access patterns
229        if config.enable_unusual_access_detection {
230            if let Some((score, factors)) = self.detect_unusual_access(&user_id, event)? {
231                anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
232            }
233        }
234
235        // Check for privilege escalation
236        if config.enable_privilege_escalation_detection {
237            if let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)? {
238                anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
239            }
240        }
241
242        // Check for data exfiltration
243        if config.enable_data_exfiltration_detection {
244            if let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)? {
245                anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
246            }
247        }
248
249        // Check for velocity anomalies
250        if config.enable_velocity_detection {
251            if let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)? {
252                anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
253            }
254        }
255
256        // Store event for future analysis
257        self.add_recent_event(event.clone());
258
259        // Calculate overall anomaly score (max of all detectors)
260        let (max_anomaly_type, max_score, all_factors) = if anomaly_scores.is_empty() {
261            (None, 0.0, vec![])
262        } else {
263            let max_entry = anomaly_scores.iter().max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).unwrap();
264            let all_factors: Vec<String> = anomaly_scores.iter().flat_map(|(_,  _, f)| f.clone()).collect();
265            (Some(max_entry.0.clone()), max_entry.1, all_factors)
266        };
267
268        let is_anomalous = max_score >= config.sensitivity;
269
270        let recommended_action = if max_score >= 0.9 {
271            RecommendedAction::RevokeAccess
272        } else if max_score >= 0.8 {
273            RecommendedAction::Block
274        } else if max_score >= 0.7 {
275            RecommendedAction::RequireMFA
276        } else if max_score >= 0.5 {
277            RecommendedAction::Alert
278        } else {
279            RecommendedAction::Monitor
280        };
281
282        let reason = if is_anomalous {
283            format!("Anomalous {:?} detected with score {:.2}", max_anomaly_type.as_ref().unwrap(), max_score)
284        } else {
285            "Normal behavior".to_string()
286        };
287
288        Ok(AnomalyResult {
289            is_anomalous,
290            score: max_score,
291            anomaly_type: max_anomaly_type,
292            reason,
293            recommended_action,
294            factors: all_factors,
295        })
296    }
297
298    /// Update user profile with new event
299    pub fn update_profile(&self, event: &AuditEvent) -> Result<()> {
300        let user_id = match event.actor() {
301            crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
302            crate::domain::entities::Actor::ApiKey { key_id, key_name: _ } => key_id.clone(),
303            _ => return Ok(()),
304        };
305
306        let mut profiles = self.user_profiles.write();
307        let profile = profiles.entry(format!("{}-{}", event.tenant_id().as_str(), user_id))
308            .or_insert_with(|| UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string()));
309
310        // Update activity patterns
311        let hour = event.timestamp().hour();
312        if !profile.typical_hours.contains(&hour) {
313            profile.typical_hours.push(hour);
314        }
315
316        // Update action frequency
317        *profile.typical_actions.entry(event.action().clone()).or_insert(0) += 1;
318
319        // Update statistics
320        profile.event_count += 1;
321        profile.last_updated = Utc::now();
322
323        // Recalculate averages (simple moving average)
324        let events_in_window = profile.event_count.min(1000);
325        profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
326
327        Ok(())
328    }
329
330    // === Detection Methods ===
331
332    fn detect_brute_force(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
333        // Detect multiple failed login attempts
334        if event.action() != &AuditAction::Login {
335            return Ok(None);
336        }
337
338        let recent = self.recent_events.read();
339        let mut recent_failures = recent.iter()
340            .filter(|e| {
341                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
342                    uid == user_id && e.action() == &AuditAction::Login
343                        && e.outcome() == &AuditOutcome::Failure
344                        && (Utc::now() - e.timestamp()) < Duration::minutes(15)
345                } else {
346                    false
347                }
348            })
349            .count();
350
351        // Include current event if it's also a failure
352        if event.outcome() == &AuditOutcome::Failure {
353            recent_failures += 1;
354        }
355
356        if recent_failures >= 5 {
357            let score = (recent_failures as f64 / 10.0).min(1.0);
358            let factors = vec![
359                format!("{} failed login attempts in 15 minutes", recent_failures),
360            ];
361            return Ok(Some((score, factors)));
362        }
363
364        Ok(None)
365    }
366
367    fn detect_unusual_access(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
368        let profiles = self.user_profiles.read();
369        let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
370
371        if let Some(profile) = profiles.get(&profile_key) {
372            if profile.event_count < self.config.read().min_baseline_events {
373                return Ok(None); // Not enough data for baseline
374            }
375
376            let mut factors = Vec::new();
377            let mut anomaly_indicators = 0;
378
379            // Check if access is outside typical hours
380            let hour = event.timestamp().hour();
381            if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
382                factors.push(format!("Access at unusual hour: {}:00", hour));
383                anomaly_indicators += 1;
384            }
385
386            // Check if action is unusual for this user
387            let action_count = profile.typical_actions.get(event.action()).copied().unwrap_or(0);
388            if action_count == 0 && profile.event_count > 50 {
389                factors.push(format!("First time performing {:?}", event.action()));
390                anomaly_indicators += 1;
391            }
392
393            if anomaly_indicators > 0 {
394                let score = (anomaly_indicators as f64 / 2.0).min(1.0);
395                return Ok(Some((score, factors)));
396            }
397        }
398
399        Ok(None)
400    }
401
402    fn detect_privilege_escalation(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
403        // Detect attempts to gain unauthorized privileges
404        let sensitive_actions = vec![
405            AuditAction::TenantUpdated,
406            AuditAction::RoleChanged,
407        ];
408
409        if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
410            let recent = self.recent_events.read();
411            let recent_privilege_attempts = recent.iter()
412                .filter(|e| {
413                    if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
414                        uid == user_id && sensitive_actions.contains(e.action())
415                            && (Utc::now() - e.timestamp()) < Duration::hours(1)
416                    } else {
417                        false
418                    }
419                })
420                .count();
421
422            if recent_privilege_attempts >= 3 {
423                let score = 0.8;
424                let factors = vec![
425                    format!("{} privilege escalation attempts in 1 hour", recent_privilege_attempts),
426                    format!("Latest action: {:?}", event.action()),
427                ];
428                return Ok(Some((score, factors)));
429            }
430        }
431
432        Ok(None)
433    }
434
435    fn detect_data_exfiltration(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
436        // Detect unusual data access patterns that might indicate exfiltration
437        if event.action() != &AuditAction::EventQueried {
438            return Ok(None);
439        }
440
441        let recent = self.recent_events.read();
442        let recent_queries = recent.iter()
443            .filter(|e| {
444                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
445                    uid == user_id && e.action() == &AuditAction::EventQueried
446                        && (Utc::now() - e.timestamp()) < Duration::hours(1)
447                } else {
448                    false
449                }
450            })
451            .count();
452
453        // Check user profile for baseline
454        let profiles = self.user_profiles.read();
455        let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
456
457        if let Some(profile) = profiles.get(&profile_key) {
458            if profile.event_count >= self.config.read().min_baseline_events {
459                // If current query rate is 5x normal, flag as anomalous
460                if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
461                    let score = 0.75;
462                    let factors = vec![
463                        format!("{} queries in 1 hour (baseline: {:.0})", recent_queries, profile.avg_actions_per_hour),
464                        "Potential data exfiltration pattern".to_string(),
465                    ];
466                    return Ok(Some((score, factors)));
467                }
468            }
469        }
470
471        Ok(None)
472    }
473
474    fn detect_velocity_anomaly(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
475        // Detect impossibly fast actions (e.g., actions from different locations in short time)
476        let recent = self.recent_events.read();
477        let very_recent = recent.iter()
478            .filter(|e| {
479                if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
480                    uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
481                } else {
482                    false
483                }
484            })
485            .count();
486
487        // More than 20 actions in 10 seconds is suspicious
488        if very_recent >= 20 {
489            let score = 0.7;
490            let factors = vec![
491                format!("{} actions in 10 seconds", very_recent),
492                "Potential automated attack or compromised credentials".to_string(),
493            ];
494            return Ok(Some((score, factors)));
495        }
496
497        Ok(None)
498    }
499
500    pub fn add_recent_event(&self, event: AuditEvent) {
501        let mut events = self.recent_events.write();
502        events.push(event);
503
504        // Keep only recent events (last 24 hours)
505        let cutoff = Utc::now() - Duration::hours(24);
506        events.retain(|e| e.timestamp() > &cutoff);
507
508        // Limit size to prevent memory issues
509        if events.len() > 10000 {
510            events.drain(0..1000);
511        }
512    }
513
514    /// Get statistics about detection
515    pub fn get_stats(&self) -> DetectionStats {
516        let profiles = self.user_profiles.read();
517        let recent = self.recent_events.read();
518
519        DetectionStats {
520            user_profiles_count: profiles.len(),
521            recent_events_count: recent.len(),
522            config: self.config.read().clone(),
523        }
524    }
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct DetectionStats {
529    pub user_profiles_count: usize,
530    pub recent_events_count: usize,
531    pub config: AnomalyDetectionConfig,
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::domain::entities::Actor;
538
539    fn create_test_event(action: AuditAction, outcome: AuditOutcome, user_id: &str) -> AuditEvent {
540        let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
541        let actor = Actor::User {
542            user_id: user_id.to_string(),
543            username: "testuser".to_string(),
544        };
545        AuditEvent::new(tenant_id, action, actor, outcome)
546    }
547
548    #[test]
549    fn test_anomaly_detector_creation() {
550        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
551        let stats = detector.get_stats();
552        assert_eq!(stats.user_profiles_count, 0);
553        assert_eq!(stats.recent_events_count, 0);
554    }
555
556    #[test]
557    fn test_normal_behavior_not_flagged() {
558        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
559        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
560
561        let result = detector.analyze_event(&event).unwrap();
562        assert!(!result.is_anomalous);
563        assert_eq!(result.recommended_action, RecommendedAction::Monitor);
564    }
565
566    #[test]
567    fn test_brute_force_detection() {
568        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
569
570        // Simulate 6 failed login attempts
571        for _ in 0..6 {
572            let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
573            detector.add_recent_event(event.clone());
574        }
575
576        // Next login attempt should be flagged
577        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
578        let result = detector.analyze_event(&event).unwrap();
579
580        assert!(result.is_anomalous);
581        assert_eq!(result.anomaly_type, Some(AnomalyType::BruteForceAttack));
582        assert!(result.score >= 0.5);
583    }
584
585    #[test]
586    fn test_profile_building() {
587        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
588        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
589
590        detector.update_profile(&event).unwrap();
591
592        let stats = detector.get_stats();
593        assert_eq!(stats.user_profiles_count, 1);
594    }
595
596    #[test]
597    fn test_velocity_anomaly() {
598        let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
599
600        // Simulate 25 actions in rapid succession
601        for _ in 0..25 {
602            let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
603            detector.add_recent_event(event.clone());
604        }
605
606        let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
607        let result = detector.analyze_event(&event).unwrap();
608
609        assert!(result.is_anomalous);
610        assert_eq!(result.anomaly_type, Some(AnomalyType::VelocityAnomaly));
611    }
612
613    #[test]
614    fn test_disabled_detection() {
615        let mut config = AnomalyDetectionConfig::default();
616        config.enabled = false;
617
618        let detector = AnomalyDetector::new(config);
619        let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
620
621        let result = detector.analyze_event(&event).unwrap();
622        assert!(!result.is_anomalous);
623    }
624}