1use crate::domain::entities::{AuditAction, AuditEvent, AuditOutcome};
11use crate::domain::value_objects::TenantId;
12use crate::error::Result;
13use chrono::{DateTime, Duration, Timelike, Utc};
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AnomalyDetectionConfig {
22 pub enabled: bool,
24
25 pub sensitivity: f64,
27
28 pub min_baseline_events: usize,
30
31 pub analysis_window_hours: i64,
33
34 pub enable_brute_force_detection: bool,
36 pub enable_unusual_access_detection: bool,
37 pub enable_privilege_escalation_detection: bool,
38 pub enable_data_exfiltration_detection: bool,
39 pub enable_velocity_detection: bool,
40}
41
42impl Default for AnomalyDetectionConfig {
43 fn default() -> Self {
44 Self {
45 enabled: true,
46 sensitivity: 0.7,
47 min_baseline_events: 100,
48 analysis_window_hours: 24,
49 enable_brute_force_detection: true,
50 enable_unusual_access_detection: true,
51 enable_privilege_escalation_detection: true,
52 enable_data_exfiltration_detection: true,
53 enable_velocity_detection: true,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AnomalyResult {
61 pub is_anomalous: bool,
63
64 pub score: f64,
66
67 pub anomaly_type: Option<AnomalyType>,
69
70 pub reason: String,
72
73 pub recommended_action: RecommendedAction,
75
76 pub factors: Vec<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub enum AnomalyType {
82 BruteForceAttack,
83 UnusualAccessPattern,
84 PrivilegeEscalation,
85 DataExfiltration,
86 VelocityAnomaly,
87 AccountCompromise,
88 SuspiciousActivity,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub enum RecommendedAction {
93 Monitor, Alert, Block, RequireMFA, RevokeAccess, }
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102struct UserProfile {
103 user_id: String,
104 tenant_id: String,
105
106 typical_hours: Vec<u32>, typical_actions: HashMap<AuditAction, usize>, typical_locations: Vec<String>, avg_actions_per_hour: f64,
113 avg_actions_per_day: f64,
114 max_actions_per_hour: usize,
115
116 avg_failure_rate: f64,
118
119 last_updated: DateTime<Utc>,
121 event_count: usize,
122}
123
124impl UserProfile {
125 fn new(user_id: String, tenant_id: String) -> Self {
126 Self {
127 user_id,
128 tenant_id,
129 typical_hours: Vec::new(),
130 typical_actions: HashMap::new(),
131 typical_locations: Vec::new(),
132 avg_actions_per_hour: 0.0,
133 avg_actions_per_day: 0.0,
134 max_actions_per_hour: 0,
135 avg_failure_rate: 0.0,
136 last_updated: Utc::now(),
137 event_count: 0,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144struct TenantProfile {
145 tenant_id: String,
146
147 typical_daily_events: f64,
149 typical_hourly_events: f64,
150 peak_hours: Vec<u32>,
151
152 active_users_per_day: f64,
154
155 avg_failure_rate: f64,
157 suspicious_event_rate: f64,
158
159 last_updated: DateTime<Utc>,
160 event_count: usize,
161}
162
163pub struct AnomalyDetector {
165 config: Arc<RwLock<AnomalyDetectionConfig>>,
166
167 user_profiles: Arc<RwLock<HashMap<String, UserProfile>>>,
169 tenant_profiles: Arc<RwLock<HashMap<String, TenantProfile>>>,
170
171 recent_events: Arc<RwLock<Vec<AuditEvent>>>,
173}
174
175impl AnomalyDetector {
176 pub fn new(config: AnomalyDetectionConfig) -> Self {
178 Self {
179 config: Arc::new(RwLock::new(config)),
180 user_profiles: Arc::new(RwLock::new(HashMap::new())),
181 tenant_profiles: Arc::new(RwLock::new(HashMap::new())),
182 recent_events: Arc::new(RwLock::new(Vec::new())),
183 }
184 }
185
186 pub fn analyze_event(&self, event: &AuditEvent) -> Result<AnomalyResult> {
188 let config = self.config.read();
189
190 if !config.enabled {
191 return Ok(AnomalyResult {
192 is_anomalous: false,
193 score: 0.0,
194 anomaly_type: None,
195 reason: "Anomaly detection disabled".to_string(),
196 recommended_action: RecommendedAction::Monitor,
197 factors: vec![],
198 });
199 }
200
201 let mut anomaly_scores: Vec<(AnomalyType, f64, Vec<String>)> = Vec::new();
202
203 let user_id = match event.actor() {
205 crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
206 crate::domain::entities::Actor::System { .. } => {
207 return Ok(AnomalyResult {
209 is_anomalous: false,
210 score: 0.0,
211 anomaly_type: None,
212 reason: "System actor".to_string(),
213 recommended_action: RecommendedAction::Monitor,
214 factors: vec![],
215 });
216 }
217 crate::domain::entities::Actor::ApiKey {
218 key_id,
219 key_name: _,
220 } => key_id.clone(),
221 };
222
223 if config.enable_brute_force_detection {
225 if let Some((score, factors)) = self.detect_brute_force(&user_id, event)? {
226 anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
227 }
228 }
229
230 if config.enable_unusual_access_detection {
232 if let Some((score, factors)) = self.detect_unusual_access(&user_id, event)? {
233 anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
234 }
235 }
236
237 if config.enable_privilege_escalation_detection {
239 if let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)? {
240 anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
241 }
242 }
243
244 if config.enable_data_exfiltration_detection {
246 if let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)? {
247 anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
248 }
249 }
250
251 if config.enable_velocity_detection {
253 if let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)? {
254 anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
255 }
256 }
257
258 self.add_recent_event(event.clone());
260
261 let (max_anomaly_type, max_score, all_factors) = if anomaly_scores.is_empty() {
263 (None, 0.0, vec![])
264 } else {
265 let max_entry = anomaly_scores
266 .iter()
267 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
268 .unwrap();
269 let all_factors: Vec<String> = anomaly_scores
270 .iter()
271 .flat_map(|(_, _, f)| f.clone())
272 .collect();
273 (Some(max_entry.0.clone()), max_entry.1, all_factors)
274 };
275
276 let is_anomalous = max_score >= config.sensitivity;
277
278 let recommended_action = if max_score >= 0.9 {
279 RecommendedAction::RevokeAccess
280 } else if max_score >= 0.8 {
281 RecommendedAction::Block
282 } else if max_score >= 0.7 {
283 RecommendedAction::RequireMFA
284 } else if max_score >= 0.5 {
285 RecommendedAction::Alert
286 } else {
287 RecommendedAction::Monitor
288 };
289
290 let reason = if is_anomalous {
291 format!(
292 "Anomalous {:?} detected with score {:.2}",
293 max_anomaly_type.as_ref().unwrap(),
294 max_score
295 )
296 } else {
297 "Normal behavior".to_string()
298 };
299
300 Ok(AnomalyResult {
301 is_anomalous,
302 score: max_score,
303 anomaly_type: max_anomaly_type,
304 reason,
305 recommended_action,
306 factors: all_factors,
307 })
308 }
309
310 pub fn update_profile(&self, event: &AuditEvent) -> Result<()> {
312 let user_id = match event.actor() {
313 crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
314 crate::domain::entities::Actor::ApiKey {
315 key_id,
316 key_name: _,
317 } => key_id.clone(),
318 _ => return Ok(()),
319 };
320
321 let mut profiles = self.user_profiles.write();
322 let profile = profiles
323 .entry(format!("{}-{}", event.tenant_id().as_str(), user_id))
324 .or_insert_with(|| {
325 UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
326 });
327
328 let hour = event.timestamp().hour();
330 if !profile.typical_hours.contains(&hour) {
331 profile.typical_hours.push(hour);
332 }
333
334 *profile
336 .typical_actions
337 .entry(event.action().clone())
338 .or_insert(0) += 1;
339
340 profile.event_count += 1;
342 profile.last_updated = Utc::now();
343
344 let events_in_window = profile.event_count.min(1000);
346 profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
347
348 Ok(())
349 }
350
351 fn detect_brute_force(
354 &self,
355 user_id: &str,
356 event: &AuditEvent,
357 ) -> Result<Option<(f64, Vec<String>)>> {
358 if event.action() != &AuditAction::Login {
360 return Ok(None);
361 }
362
363 let recent = self.recent_events.read();
364 let mut recent_failures = recent
365 .iter()
366 .filter(|e| {
367 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
368 uid == user_id
369 && e.action() == &AuditAction::Login
370 && e.outcome() == &AuditOutcome::Failure
371 && (Utc::now() - e.timestamp()) < Duration::minutes(15)
372 } else {
373 false
374 }
375 })
376 .count();
377
378 if event.outcome() == &AuditOutcome::Failure {
380 recent_failures += 1;
381 }
382
383 if recent_failures >= 5 {
384 let score = (recent_failures as f64 / 10.0).min(1.0);
385 let factors = vec![format!(
386 "{} failed login attempts in 15 minutes",
387 recent_failures
388 )];
389 return Ok(Some((score, factors)));
390 }
391
392 Ok(None)
393 }
394
395 fn detect_unusual_access(
396 &self,
397 user_id: &str,
398 event: &AuditEvent,
399 ) -> Result<Option<(f64, Vec<String>)>> {
400 let profiles = self.user_profiles.read();
401 let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
402
403 if let Some(profile) = profiles.get(&profile_key) {
404 if profile.event_count < self.config.read().min_baseline_events {
405 return Ok(None); }
407
408 let mut factors = Vec::new();
409 let mut anomaly_indicators = 0;
410
411 let hour = event.timestamp().hour();
413 if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
414 factors.push(format!("Access at unusual hour: {}:00", hour));
415 anomaly_indicators += 1;
416 }
417
418 let action_count = profile
420 .typical_actions
421 .get(event.action())
422 .copied()
423 .unwrap_or(0);
424 if action_count == 0 && profile.event_count > 50 {
425 factors.push(format!("First time performing {:?}", event.action()));
426 anomaly_indicators += 1;
427 }
428
429 if anomaly_indicators > 0 {
430 let score = (anomaly_indicators as f64 / 2.0).min(1.0);
431 return Ok(Some((score, factors)));
432 }
433 }
434
435 Ok(None)
436 }
437
438 fn detect_privilege_escalation(
439 &self,
440 user_id: &str,
441 event: &AuditEvent,
442 ) -> Result<Option<(f64, Vec<String>)>> {
443 let sensitive_actions = vec![AuditAction::TenantUpdated, AuditAction::RoleChanged];
445
446 if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
447 let recent = self.recent_events.read();
448 let recent_privilege_attempts = recent
449 .iter()
450 .filter(|e| {
451 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
452 uid == user_id
453 && sensitive_actions.contains(e.action())
454 && (Utc::now() - e.timestamp()) < Duration::hours(1)
455 } else {
456 false
457 }
458 })
459 .count();
460
461 if recent_privilege_attempts >= 3 {
462 let score = 0.8;
463 let factors = vec![
464 format!(
465 "{} privilege escalation attempts in 1 hour",
466 recent_privilege_attempts
467 ),
468 format!("Latest action: {:?}", event.action()),
469 ];
470 return Ok(Some((score, factors)));
471 }
472 }
473
474 Ok(None)
475 }
476
477 fn detect_data_exfiltration(
478 &self,
479 user_id: &str,
480 event: &AuditEvent,
481 ) -> Result<Option<(f64, Vec<String>)>> {
482 if event.action() != &AuditAction::EventQueried {
484 return Ok(None);
485 }
486
487 let recent = self.recent_events.read();
488 let recent_queries = recent
489 .iter()
490 .filter(|e| {
491 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
492 uid == user_id
493 && e.action() == &AuditAction::EventQueried
494 && (Utc::now() - e.timestamp()) < Duration::hours(1)
495 } else {
496 false
497 }
498 })
499 .count();
500
501 let profiles = self.user_profiles.read();
503 let profile_key = format!("{}-{}", event.tenant_id().as_str(), user_id);
504
505 if let Some(profile) = profiles.get(&profile_key) {
506 if profile.event_count >= self.config.read().min_baseline_events {
507 if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
509 let score = 0.75;
510 let factors = vec![
511 format!(
512 "{} queries in 1 hour (baseline: {:.0})",
513 recent_queries, profile.avg_actions_per_hour
514 ),
515 "Potential data exfiltration pattern".to_string(),
516 ];
517 return Ok(Some((score, factors)));
518 }
519 }
520 }
521
522 Ok(None)
523 }
524
525 fn detect_velocity_anomaly(
526 &self,
527 user_id: &str,
528 event: &AuditEvent,
529 ) -> Result<Option<(f64, Vec<String>)>> {
530 let recent = self.recent_events.read();
532 let very_recent = recent
533 .iter()
534 .filter(|e| {
535 if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
536 uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
537 } else {
538 false
539 }
540 })
541 .count();
542
543 if very_recent >= 20 {
545 let score = 0.7;
546 let factors = vec![
547 format!("{} actions in 10 seconds", very_recent),
548 "Potential automated attack or compromised credentials".to_string(),
549 ];
550 return Ok(Some((score, factors)));
551 }
552
553 Ok(None)
554 }
555
556 pub fn add_recent_event(&self, event: AuditEvent) {
557 let mut events = self.recent_events.write();
558 events.push(event);
559
560 let cutoff = Utc::now() - Duration::hours(24);
562 events.retain(|e| e.timestamp() > &cutoff);
563
564 if events.len() > 10000 {
566 events.drain(0..1000);
567 }
568 }
569
570 pub fn get_stats(&self) -> DetectionStats {
572 let profiles = self.user_profiles.read();
573 let recent = self.recent_events.read();
574
575 DetectionStats {
576 user_profiles_count: profiles.len(),
577 recent_events_count: recent.len(),
578 config: self.config.read().clone(),
579 }
580 }
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct DetectionStats {
585 pub user_profiles_count: usize,
586 pub recent_events_count: usize,
587 pub config: AnomalyDetectionConfig,
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::domain::entities::Actor;
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 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 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 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}