1use crate::domain::entities::{AuditAction, AuditEvent, AuditOutcome};
11use crate::error::Result;
12use chrono::{DateTime, Duration, Timelike, Utc};
13use dashmap::DashMap;
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use std::{collections::HashMap, sync::Arc};
17
18#[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<DashMap<String, UserProfile>>,
168 tenant_profiles: Arc<DashMap<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(DashMap::new()),
180 tenant_profiles: Arc::new(DashMap::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 && let Some((score, factors)) = self.detect_brute_force(&user_id, event)?
225 {
226 anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
227 }
228
229 if config.enable_unusual_access_detection
231 && let Some((score, factors)) = self.detect_unusual_access(&user_id, event)?
232 {
233 anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
234 }
235
236 if config.enable_privilege_escalation_detection
238 && let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)?
239 {
240 anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
241 }
242
243 if config.enable_data_exfiltration_detection
245 && let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)?
246 {
247 anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
248 }
249
250 if config.enable_velocity_detection
252 && let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)?
253 {
254 anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
255 }
256
257 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 crate::domain::entities::Actor::System { .. } => return Ok(()),
318 };
319
320 let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
321 let mut profile = self.user_profiles.entry(profile_key).or_insert_with(|| {
322 UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
323 });
324
325 let hour = event.timestamp().hour();
327 if !profile.typical_hours.contains(&hour) {
328 profile.typical_hours.push(hour);
329 }
330
331 *profile
333 .typical_actions
334 .entry(event.action().clone())
335 .or_insert(0) += 1;
336
337 profile.event_count += 1;
339 profile.last_updated = Utc::now();
340
341 let events_in_window = profile.event_count.min(1000);
343 profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
344
345 Ok(())
346 }
347
348 fn detect_brute_force(
351 &self,
352 user_id: &str,
353 event: &AuditEvent,
354 ) -> Result<Option<(f64, Vec<String>)>> {
355 if event.action() != &AuditAction::Login {
357 return Ok(None);
358 }
359
360 let recent = self.recent_events.read();
361 let mut recent_failures = recent
362 .iter()
363 .filter(|e| {
364 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
365 uid == user_id
366 && e.action() == &AuditAction::Login
367 && e.outcome() == &AuditOutcome::Failure
368 && (Utc::now() - e.timestamp()) < Duration::minutes(15)
369 } else {
370 false
371 }
372 })
373 .count();
374
375 if event.outcome() == &AuditOutcome::Failure {
377 recent_failures += 1;
378 }
379
380 if recent_failures >= 5 {
381 let score = (recent_failures as f64 / 10.0).min(1.0);
382 let factors = vec![format!(
383 "{} failed login attempts in 15 minutes",
384 recent_failures
385 )];
386 return Ok(Some((score, factors)));
387 }
388
389 Ok(None)
390 }
391
392 fn detect_unusual_access(
393 &self,
394 user_id: &str,
395 event: &AuditEvent,
396 ) -> Result<Option<(f64, Vec<String>)>> {
397 let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
398
399 if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
400 let profile = profile_ref.value();
401 if profile.event_count < self.config.read().min_baseline_events {
402 return Ok(None); }
404
405 let mut factors = Vec::new();
406 let mut anomaly_indicators = 0;
407
408 let hour = event.timestamp().hour();
410 if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
411 factors.push(format!("Access at unusual hour: {hour}:00"));
412 anomaly_indicators += 1;
413 }
414
415 let action_count = profile
417 .typical_actions
418 .get(event.action())
419 .copied()
420 .unwrap_or(0);
421 if action_count == 0 && profile.event_count > 50 {
422 factors.push(format!("First time performing {:?}", event.action()));
423 anomaly_indicators += 1;
424 }
425
426 if anomaly_indicators > 0 {
427 let score = (f64::from(anomaly_indicators) / 2.0).min(1.0);
428 return Ok(Some((score, factors)));
429 }
430 }
431
432 Ok(None)
433 }
434
435 fn detect_privilege_escalation(
436 &self,
437 user_id: &str,
438 event: &AuditEvent,
439 ) -> Result<Option<(f64, Vec<String>)>> {
440 let sensitive_actions = [AuditAction::TenantUpdated, AuditAction::RoleChanged];
442
443 if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
444 let recent = self.recent_events.read();
445 let recent_privilege_attempts = recent
446 .iter()
447 .filter(|e| {
448 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
449 uid == user_id
450 && sensitive_actions.contains(e.action())
451 && (Utc::now() - e.timestamp()) < Duration::hours(1)
452 } else {
453 false
454 }
455 })
456 .count();
457
458 if recent_privilege_attempts >= 3 {
459 let score = 0.8;
460 let factors = vec![
461 format!(
462 "{} privilege escalation attempts in 1 hour",
463 recent_privilege_attempts
464 ),
465 format!("Latest action: {:?}", event.action()),
466 ];
467 return Ok(Some((score, factors)));
468 }
469 }
470
471 Ok(None)
472 }
473
474 fn detect_data_exfiltration(
475 &self,
476 user_id: &str,
477 event: &AuditEvent,
478 ) -> Result<Option<(f64, Vec<String>)>> {
479 if event.action() != &AuditAction::EventQueried {
481 return Ok(None);
482 }
483
484 let recent = self.recent_events.read();
485 let recent_queries = recent
486 .iter()
487 .filter(|e| {
488 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
489 uid == user_id
490 && e.action() == &AuditAction::EventQueried
491 && (Utc::now() - e.timestamp()) < Duration::hours(1)
492 } else {
493 false
494 }
495 })
496 .count();
497
498 let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
500
501 if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
502 let profile = profile_ref.value();
503 if profile.event_count >= self.config.read().min_baseline_events {
504 if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
506 let score = 0.75;
507 let factors = vec![
508 format!(
509 "{} queries in 1 hour (baseline: {:.0})",
510 recent_queries, profile.avg_actions_per_hour
511 ),
512 "Potential data exfiltration pattern".to_string(),
513 ];
514 return Ok(Some((score, factors)));
515 }
516 }
517 }
518
519 Ok(None)
520 }
521
522 fn detect_velocity_anomaly(
523 &self,
524 user_id: &str,
525 event: &AuditEvent,
526 ) -> Result<Option<(f64, Vec<String>)>> {
527 let recent = self.recent_events.read();
529 let very_recent = recent
530 .iter()
531 .filter(|e| {
532 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
533 uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
534 } else {
535 false
536 }
537 })
538 .count();
539
540 if very_recent >= 20 {
542 let score = 0.7;
543 let factors = vec![
544 format!("{very_recent} actions in 10 seconds"),
545 "Potential automated attack or compromised credentials".to_string(),
546 ];
547 return Ok(Some((score, factors)));
548 }
549
550 Ok(None)
551 }
552
553 pub fn add_recent_event(&self, event: AuditEvent) {
554 let mut events = self.recent_events.write();
555 events.push(event);
556
557 let cutoff = Utc::now() - Duration::hours(24);
559 events.retain(|e| e.timestamp() > &cutoff);
560
561 if events.len() > 10000 {
563 events.drain(0..1000);
564 }
565 }
566
567 pub fn get_stats(&self) -> DetectionStats {
569 let recent = self.recent_events.read();
570
571 DetectionStats {
572 user_profiles_count: self.user_profiles.len(),
573 recent_events_count: recent.len(),
574 config: self.config.read().clone(),
575 }
576 }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct DetectionStats {
581 pub user_profiles_count: usize,
582 pub recent_events_count: usize,
583 pub config: AnomalyDetectionConfig,
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use crate::domain::{entities::Actor, value_objects::TenantId};
590
591 fn create_test_event(action: AuditAction, outcome: AuditOutcome, user_id: &str) -> AuditEvent {
592 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
593 let actor = Actor::User {
594 user_id: user_id.to_string(),
595 username: "testuser".to_string(),
596 };
597 AuditEvent::new(tenant_id, action, actor, outcome)
598 }
599
600 fn create_system_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
601 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
602 let actor = Actor::System {
603 component: "test-service".to_string(),
604 };
605 AuditEvent::new(tenant_id, action, actor, outcome)
606 }
607
608 fn create_api_key_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
609 let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
610 let actor = Actor::ApiKey {
611 key_id: "key-123".to_string(),
612 key_name: "test-key".to_string(),
613 };
614 AuditEvent::new(tenant_id, action, actor, outcome)
615 }
616
617 #[test]
618 fn test_anomaly_detector_creation() {
619 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
620 let stats = detector.get_stats();
621 assert_eq!(stats.user_profiles_count, 0);
622 assert_eq!(stats.recent_events_count, 0);
623 }
624
625 #[test]
626 fn test_normal_behavior_not_flagged() {
627 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
628 let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
629
630 let result = detector.analyze_event(&event).unwrap();
631 assert!(!result.is_anomalous);
632 assert_eq!(result.recommended_action, RecommendedAction::Monitor);
633 }
634
635 #[test]
636 fn test_brute_force_detection() {
637 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
638
639 for _ in 0..6 {
641 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
642 detector.add_recent_event(event.clone());
643 }
644
645 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
647 let result = detector.analyze_event(&event).unwrap();
648
649 assert!(result.is_anomalous);
650 assert_eq!(result.anomaly_type, Some(AnomalyType::BruteForceAttack));
651 assert!(result.score >= 0.5);
652 }
653
654 #[test]
655 fn test_profile_building() {
656 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
657 let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
658
659 detector.update_profile(&event).unwrap();
660
661 let stats = detector.get_stats();
662 assert_eq!(stats.user_profiles_count, 1);
663 }
664
665 #[test]
666 fn test_velocity_anomaly() {
667 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
668
669 for _ in 0..25 {
671 let event =
672 create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
673 detector.add_recent_event(event.clone());
674 }
675
676 let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
677 let result = detector.analyze_event(&event).unwrap();
678
679 assert!(result.is_anomalous);
680 assert_eq!(result.anomaly_type, Some(AnomalyType::VelocityAnomaly));
681 }
682
683 #[test]
684 fn test_disabled_detection() {
685 let config = AnomalyDetectionConfig {
686 enabled: false,
687 ..Default::default()
688 };
689
690 let detector = AnomalyDetector::new(config);
691 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
692
693 let result = detector.analyze_event(&event).unwrap();
694 assert!(!result.is_anomalous);
695 }
696
697 #[test]
698 fn test_default_config() {
699 let config = AnomalyDetectionConfig::default();
700 assert!(config.enabled);
701 assert_eq!(config.sensitivity, 0.7);
702 assert_eq!(config.min_baseline_events, 100);
703 assert_eq!(config.analysis_window_hours, 24);
704 assert!(config.enable_brute_force_detection);
705 assert!(config.enable_unusual_access_detection);
706 assert!(config.enable_privilege_escalation_detection);
707 assert!(config.enable_data_exfiltration_detection);
708 assert!(config.enable_velocity_detection);
709 }
710
711 #[test]
712 fn test_config_serde() {
713 let config = AnomalyDetectionConfig::default();
714 let json = serde_json::to_string(&config).unwrap();
715 let parsed: AnomalyDetectionConfig = serde_json::from_str(&json).unwrap();
716 assert_eq!(parsed.enabled, config.enabled);
717 assert_eq!(parsed.sensitivity, config.sensitivity);
718 }
719
720 #[test]
721 fn test_anomaly_type_equality() {
722 assert_eq!(AnomalyType::BruteForceAttack, AnomalyType::BruteForceAttack);
723 assert_ne!(AnomalyType::BruteForceAttack, AnomalyType::DataExfiltration);
724 }
725
726 #[test]
727 fn test_recommended_action_equality() {
728 assert_eq!(RecommendedAction::Monitor, RecommendedAction::Monitor);
729 assert_ne!(RecommendedAction::Monitor, RecommendedAction::Block);
730 }
731
732 #[test]
733 fn test_system_actor_not_flagged() {
734 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
735 let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
736
737 let result = detector.analyze_event(&event).unwrap();
738 assert!(!result.is_anomalous);
739 assert_eq!(result.reason, "System actor");
740 }
741
742 #[test]
743 fn test_api_key_actor_analyzed() {
744 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
745 let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
746
747 let result = detector.analyze_event(&event).unwrap();
748 assert!(!result.is_anomalous);
749 }
750
751 #[test]
752 fn test_update_profile_api_key() {
753 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
754 let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
755
756 detector.update_profile(&event).unwrap();
757
758 let stats = detector.get_stats();
759 assert_eq!(stats.user_profiles_count, 1);
760 }
761
762 #[test]
763 fn test_update_profile_system() {
764 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
765 let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
766
767 detector.update_profile(&event).unwrap();
768
769 let stats = detector.get_stats();
770 assert_eq!(stats.user_profiles_count, 0);
772 }
773
774 #[test]
775 fn test_recent_events_pruning() {
776 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
777
778 for i in 0..10050 {
780 let event = create_test_event(
781 AuditAction::EventQueried,
782 AuditOutcome::Success,
783 &format!("user{}", i % 100),
784 );
785 detector.add_recent_event(event);
786 }
787
788 let stats = detector.get_stats();
789 assert!(stats.recent_events_count <= 10050);
791 }
792
793 #[test]
794 fn test_detection_stats_serde() {
795 let stats = DetectionStats {
796 user_profiles_count: 10,
797 recent_events_count: 100,
798 config: AnomalyDetectionConfig::default(),
799 };
800
801 let json = serde_json::to_string(&stats).unwrap();
802 assert!(json.contains("\"user_profiles_count\":10"));
803 assert!(json.contains("\"recent_events_count\":100"));
804 }
805
806 #[test]
807 fn test_anomaly_result_serde() {
808 let result = AnomalyResult {
809 is_anomalous: true,
810 score: 0.85,
811 anomaly_type: Some(AnomalyType::BruteForceAttack),
812 reason: "Test reason".to_string(),
813 recommended_action: RecommendedAction::Block,
814 factors: vec!["factor1".to_string(), "factor2".to_string()],
815 };
816
817 let json = serde_json::to_string(&result).unwrap();
818 let parsed: AnomalyResult = serde_json::from_str(&json).unwrap();
819 assert_eq!(parsed.is_anomalous, result.is_anomalous);
820 assert_eq!(parsed.score, result.score);
821 assert_eq!(parsed.anomaly_type, result.anomaly_type);
822 }
823
824 #[test]
825 fn test_recommended_action_for_high_score() {
826 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
827
828 for _ in 0..15 {
830 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
831 detector.add_recent_event(event.clone());
832 }
833
834 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
835 let result = detector.analyze_event(&event).unwrap();
836
837 assert!(result.is_anomalous);
838 if result.score >= 0.9 {
840 assert_eq!(result.recommended_action, RecommendedAction::RevokeAccess);
841 } else if result.score >= 0.8 {
842 assert_eq!(result.recommended_action, RecommendedAction::Block);
843 }
844 }
845
846 #[test]
847 fn test_successful_login_after_failures() {
848 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
849
850 for _ in 0..3 {
852 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
853 detector.add_recent_event(event);
854 }
855
856 let event = create_test_event(AuditAction::Login, AuditOutcome::Success, "user1");
858 let result = detector.analyze_event(&event).unwrap();
859
860 assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
862 }
863
864 #[test]
865 fn test_privilege_escalation_detection() {
866 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
867
868 for _ in 0..4 {
870 let event =
871 create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
872 detector.add_recent_event(event);
873 }
874
875 let event = create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
876 let result = detector.analyze_event(&event).unwrap();
877
878 assert!(result.is_anomalous);
879 assert_eq!(result.anomaly_type, Some(AnomalyType::PrivilegeEscalation));
880 }
881
882 #[test]
883 fn test_non_login_action_not_brute_force() {
884 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
885
886 for _ in 0..10 {
888 let event =
889 create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
890 detector.add_recent_event(event);
891 }
892
893 let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
894 let result = detector.analyze_event(&event).unwrap();
895
896 assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
898 }
899
900 #[test]
901 fn test_multiple_users_independent() {
902 let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
903
904 for _ in 0..6 {
906 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
907 detector.add_recent_event(event);
908 }
909
910 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user2");
912 let result = detector.analyze_event(&event).unwrap();
913
914 assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
916 }
917
918 #[test]
919 fn test_anomaly_types_serde() {
920 let types = vec![
921 AnomalyType::BruteForceAttack,
922 AnomalyType::UnusualAccessPattern,
923 AnomalyType::PrivilegeEscalation,
924 AnomalyType::DataExfiltration,
925 AnomalyType::VelocityAnomaly,
926 AnomalyType::AccountCompromise,
927 AnomalyType::SuspiciousActivity,
928 ];
929
930 for anomaly_type in types {
931 let json = serde_json::to_string(&anomaly_type).unwrap();
932 let parsed: AnomalyType = serde_json::from_str(&json).unwrap();
933 assert_eq!(parsed, anomaly_type);
934 }
935 }
936
937 #[test]
938 fn test_recommended_actions_serde() {
939 let actions = vec![
940 RecommendedAction::Monitor,
941 RecommendedAction::Alert,
942 RecommendedAction::Block,
943 RecommendedAction::RequireMFA,
944 RecommendedAction::RevokeAccess,
945 ];
946
947 for action in actions {
948 let json = serde_json::to_string(&action).unwrap();
949 let parsed: RecommendedAction = serde_json::from_str(&json).unwrap();
950 assert_eq!(parsed, action);
951 }
952 }
953
954 #[test]
955 fn test_disabled_individual_detectors() {
956 let config = AnomalyDetectionConfig {
957 enabled: true,
958 enable_brute_force_detection: false,
959 enable_velocity_detection: false,
960 ..Default::default()
961 };
962
963 let detector = AnomalyDetector::new(config);
964
965 for _ in 0..10 {
967 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
968 detector.add_recent_event(event);
969 }
970
971 let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
972 let result = detector.analyze_event(&event).unwrap();
973
974 assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
976 }
977}