1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AnomalyDetectionConfig {
23 pub enabled: bool,
25
26 pub sensitivity: f64,
28
29 pub min_baseline_events: usize,
31
32 pub analysis_window_hours: i64,
34
35 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#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AnomalyResult {
62 pub is_anomalous: bool,
64
65 pub score: f64,
67
68 pub anomaly_type: Option<AnomalyType>,
70
71 pub reason: String,
73
74 pub recommended_action: RecommendedAction,
76
77 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, Alert, Block, RequireMFA, RevokeAccess, }
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103struct UserProfile {
104 user_id: String,
105 tenant_id: String,
106
107 typical_hours: Vec<u32>, typical_actions: HashMap<AuditAction, usize>, typical_locations: Vec<String>, avg_actions_per_hour: f64,
114 avg_actions_per_day: f64,
115 max_actions_per_hour: usize,
116
117 avg_failure_rate: f64,
119
120 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#[derive(Debug, Clone, Serialize, Deserialize)]
145struct TenantProfile {
146 tenant_id: String,
147
148 typical_daily_events: f64,
150 typical_hourly_events: f64,
151 peak_hours: Vec<u32>,
152
153 active_users_per_day: f64,
155
156 avg_failure_rate: f64,
158 suspicious_event_rate: f64,
159
160 last_updated: DateTime<Utc>,
161 event_count: usize,
162}
163
164pub struct AnomalyDetector {
166 config: Arc<RwLock<AnomalyDetectionConfig>>,
167
168 user_profiles: Arc<RwLock<HashMap<String, UserProfile>>>,
170 tenant_profiles: Arc<RwLock<HashMap<String, TenantProfile>>>,
171
172 recent_events: Arc<RwLock<Vec<AuditEvent>>>,
174}
175
176impl AnomalyDetector {
177 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 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 let user_id = match event.actor() {
206 crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
207 crate::domain::entities::Actor::System { .. } => {
208 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 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 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 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 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 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 self.add_recent_event(event.clone());
258
259 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 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 let hour = event.timestamp().hour();
312 if !profile.typical_hours.contains(&hour) {
313 profile.typical_hours.push(hour);
314 }
315
316 *profile.typical_actions.entry(event.action().clone()).or_insert(0) += 1;
318
319 profile.event_count += 1;
321 profile.last_updated = Utc::now();
322
323 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 fn detect_brute_force(&self, user_id: &str, event: &AuditEvent) -> Result<Option<(f64, Vec<String>)>> {
333 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 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); }
375
376 let mut factors = Vec::new();
377 let mut anomaly_indicators = 0;
378
379 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 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 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 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 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 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 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 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 let cutoff = Utc::now() - Duration::hours(24);
506 events.retain(|e| e.timestamp() > &cutoff);
507
508 if events.len() > 10000 {
510 events.drain(0..1000);
511 }
512 }
513
514 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 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 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 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}