1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AnomalyDetectionConfig {
21 pub enabled: bool,
23
24 pub sensitivity: f64,
26
27 pub min_baseline_events: usize,
29
30 pub analysis_window_hours: i64,
32
33 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#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AnomalyResult {
60 pub is_anomalous: bool,
62
63 pub score: f64,
65
66 pub anomaly_type: Option<AnomalyType>,
68
69 pub reason: String,
71
72 pub recommended_action: RecommendedAction,
74
75 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, Alert, Block, RequireMFA, RevokeAccess, }
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101struct UserProfile {
102 user_id: String,
103 tenant_id: String,
104
105 typical_hours: Vec<u32>, typical_actions: HashMap<AuditAction, usize>, typical_locations: Vec<String>, avg_actions_per_hour: f64,
112 avg_actions_per_day: f64,
113 max_actions_per_hour: usize,
114
115 avg_failure_rate: f64,
117
118 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#[derive(Debug, Clone, Serialize, Deserialize)]
143struct TenantProfile {
144 tenant_id: String,
145
146 typical_daily_events: f64,
148 typical_hourly_events: f64,
149 peak_hours: Vec<u32>,
150
151 active_users_per_day: f64,
153
154 avg_failure_rate: f64,
156 suspicious_event_rate: f64,
157
158 last_updated: DateTime<Utc>,
159 event_count: usize,
160}
161
162pub struct AnomalyDetector {
164 config: Arc<RwLock<AnomalyDetectionConfig>>,
165
166 user_profiles: Arc<RwLock<HashMap<String, UserProfile>>>,
168 tenant_profiles: Arc<RwLock<HashMap<String, TenantProfile>>>,
169
170 recent_events: Arc<RwLock<Vec<AuditEvent>>>,
172}
173
174impl AnomalyDetector {
175 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 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 let user_id = match event.actor() {
204 crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
205 crate::domain::entities::Actor::System { .. } => {
206 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 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 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 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 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 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 self.add_recent_event(event.clone());
259
260 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 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!("{}-{user_id}", event.tenant_id().as_str()))
323 .or_insert_with(|| {
324 UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
325 });
326
327 let hour = event.timestamp().hour();
329 if !profile.typical_hours.contains(&hour) {
330 profile.typical_hours.push(hour);
331 }
332
333 *profile
335 .typical_actions
336 .entry(event.action().clone())
337 .or_insert(0) += 1;
338
339 profile.event_count += 1;
341 profile.last_updated = Utc::now();
342
343 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 fn detect_brute_force(
353 &self,
354 user_id: &str,
355 event: &AuditEvent,
356 ) -> Result<Option<(f64, Vec<String>)>> {
357 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 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!("{}-{user_id}", event.tenant_id().as_str());
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); }
406
407 let mut factors = Vec::new();
408 let mut anomaly_indicators = 0;
409
410 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: {hour}:00"));
414 anomaly_indicators += 1;
415 }
416
417 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 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 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 let profiles = self.user_profiles.read();
502 let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
503
504 if let Some(profile) = profiles.get(&profile_key) {
505 if profile.event_count >= self.config.read().min_baseline_events {
506 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 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 if very_recent >= 20 {
544 let score = 0.7;
545 let factors = vec![
546 format!("{very_recent} actions in 10 seconds"),
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 let cutoff = Utc::now() - Duration::hours(24);
561 events.retain(|e| e.timestamp() > &cutoff);
562
563 if events.len() > 10000 {
565 events.drain(0..1000);
566 }
567 }
568
569 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 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 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 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 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 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 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 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 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 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 for _ in 0..3 {
856 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
857 detector.add_recent_event(event);
858 }
859
860 let event = create_test_event(AuditAction::Login, AuditOutcome::Success, "user1");
862 let result = detector.analyze_event(&event).unwrap();
863
864 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 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 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 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 for _ in 0..6 {
910 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
911 detector.add_recent_event(event);
912 }
913
914 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user2");
916 let result = detector.analyze_event(&event).unwrap();
917
918 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 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 assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
980 }
981}