use crate::domain::entities::{AuditAction, AuditEvent, AuditOutcome};
use crate::error::Result;
use chrono::{DateTime, Duration, Timelike, Utc};
use dashmap::DashMap;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyDetectionConfig {
pub enabled: bool,
pub sensitivity: f64,
pub min_baseline_events: usize,
pub analysis_window_hours: i64,
pub enable_brute_force_detection: bool,
pub enable_unusual_access_detection: bool,
pub enable_privilege_escalation_detection: bool,
pub enable_data_exfiltration_detection: bool,
pub enable_velocity_detection: bool,
}
impl Default for AnomalyDetectionConfig {
fn default() -> Self {
Self {
enabled: true,
sensitivity: 0.7,
min_baseline_events: 100,
analysis_window_hours: 24,
enable_brute_force_detection: true,
enable_unusual_access_detection: true,
enable_privilege_escalation_detection: true,
enable_data_exfiltration_detection: true,
enable_velocity_detection: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyResult {
pub is_anomalous: bool,
pub score: f64,
pub anomaly_type: Option<AnomalyType>,
pub reason: String,
pub recommended_action: RecommendedAction,
pub factors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AnomalyType {
BruteForceAttack,
UnusualAccessPattern,
PrivilegeEscalation,
DataExfiltration,
VelocityAnomaly,
AccountCompromise,
SuspiciousActivity,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RecommendedAction {
Monitor, Alert, Block, RequireMFA, RevokeAccess, }
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserProfile {
user_id: String,
tenant_id: String,
typical_hours: Vec<u32>, typical_actions: HashMap<AuditAction, usize>, typical_locations: Vec<String>,
avg_actions_per_hour: f64,
avg_actions_per_day: f64,
max_actions_per_hour: usize,
avg_failure_rate: f64,
last_updated: DateTime<Utc>,
event_count: usize,
}
impl UserProfile {
fn new(user_id: String, tenant_id: String) -> Self {
Self {
user_id,
tenant_id,
typical_hours: Vec::new(),
typical_actions: HashMap::new(),
typical_locations: Vec::new(),
avg_actions_per_hour: 0.0,
avg_actions_per_day: 0.0,
max_actions_per_hour: 0,
avg_failure_rate: 0.0,
last_updated: Utc::now(),
event_count: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TenantProfile {
tenant_id: String,
typical_daily_events: f64,
typical_hourly_events: f64,
peak_hours: Vec<u32>,
active_users_per_day: f64,
avg_failure_rate: f64,
suspicious_event_rate: f64,
last_updated: DateTime<Utc>,
event_count: usize,
}
pub struct AnomalyDetector {
config: Arc<RwLock<AnomalyDetectionConfig>>,
user_profiles: Arc<DashMap<String, UserProfile>>,
tenant_profiles: Arc<DashMap<String, TenantProfile>>,
recent_events: Arc<RwLock<Vec<AuditEvent>>>,
}
impl AnomalyDetector {
pub fn new(config: AnomalyDetectionConfig) -> Self {
Self {
config: Arc::new(RwLock::new(config)),
user_profiles: Arc::new(DashMap::new()),
tenant_profiles: Arc::new(DashMap::new()),
recent_events: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn analyze_event(&self, event: &AuditEvent) -> Result<AnomalyResult> {
let config = self.config.read();
if !config.enabled {
return Ok(AnomalyResult {
is_anomalous: false,
score: 0.0,
anomaly_type: None,
reason: "Anomaly detection disabled".to_string(),
recommended_action: RecommendedAction::Monitor,
factors: vec![],
});
}
let mut anomaly_scores: Vec<(AnomalyType, f64, Vec<String>)> = Vec::new();
let user_id = match event.actor() {
crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
crate::domain::entities::Actor::System { .. } => {
return Ok(AnomalyResult {
is_anomalous: false,
score: 0.0,
anomaly_type: None,
reason: "System actor".to_string(),
recommended_action: RecommendedAction::Monitor,
factors: vec![],
});
}
crate::domain::entities::Actor::ApiKey {
key_id,
key_name: _,
} => key_id.clone(),
};
if config.enable_brute_force_detection
&& let Some((score, factors)) = self.detect_brute_force(&user_id, event)?
{
anomaly_scores.push((AnomalyType::BruteForceAttack, score, factors));
}
if config.enable_unusual_access_detection
&& let Some((score, factors)) = self.detect_unusual_access(&user_id, event)?
{
anomaly_scores.push((AnomalyType::UnusualAccessPattern, score, factors));
}
if config.enable_privilege_escalation_detection
&& let Some((score, factors)) = self.detect_privilege_escalation(&user_id, event)?
{
anomaly_scores.push((AnomalyType::PrivilegeEscalation, score, factors));
}
if config.enable_data_exfiltration_detection
&& let Some((score, factors)) = self.detect_data_exfiltration(&user_id, event)?
{
anomaly_scores.push((AnomalyType::DataExfiltration, score, factors));
}
if config.enable_velocity_detection
&& let Some((score, factors)) = self.detect_velocity_anomaly(&user_id, event)?
{
anomaly_scores.push((AnomalyType::VelocityAnomaly, score, factors));
}
self.add_recent_event(event.clone());
let (max_anomaly_type, max_score, all_factors) = if anomaly_scores.is_empty() {
(None, 0.0, vec![])
} else {
let max_entry = anomaly_scores
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap();
let all_factors: Vec<String> = anomaly_scores
.iter()
.flat_map(|(_, _, f)| f.clone())
.collect();
(Some(max_entry.0.clone()), max_entry.1, all_factors)
};
let is_anomalous = max_score >= config.sensitivity;
let recommended_action = if max_score >= 0.9 {
RecommendedAction::RevokeAccess
} else if max_score >= 0.8 {
RecommendedAction::Block
} else if max_score >= 0.7 {
RecommendedAction::RequireMFA
} else if max_score >= 0.5 {
RecommendedAction::Alert
} else {
RecommendedAction::Monitor
};
let reason = if is_anomalous {
format!(
"Anomalous {:?} detected with score {:.2}",
max_anomaly_type.as_ref().unwrap(),
max_score
)
} else {
"Normal behavior".to_string()
};
Ok(AnomalyResult {
is_anomalous,
score: max_score,
anomaly_type: max_anomaly_type,
reason,
recommended_action,
factors: all_factors,
})
}
pub fn update_profile(&self, event: &AuditEvent) -> Result<()> {
let user_id = match event.actor() {
crate::domain::entities::Actor::User { user_id, .. } => user_id.clone(),
crate::domain::entities::Actor::ApiKey {
key_id,
key_name: _,
} => key_id.clone(),
crate::domain::entities::Actor::System { .. } => return Ok(()),
};
let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
let mut profile = self.user_profiles.entry(profile_key).or_insert_with(|| {
UserProfile::new(user_id.clone(), event.tenant_id().as_str().to_string())
});
let hour = event.timestamp().hour();
if !profile.typical_hours.contains(&hour) {
profile.typical_hours.push(hour);
}
*profile
.typical_actions
.entry(event.action().clone())
.or_insert(0) += 1;
profile.event_count += 1;
profile.last_updated = Utc::now();
let events_in_window = profile.event_count.min(1000);
profile.avg_actions_per_hour = events_in_window as f64 / 24.0;
Ok(())
}
fn detect_brute_force(
&self,
user_id: &str,
event: &AuditEvent,
) -> Result<Option<(f64, Vec<String>)>> {
if event.action() != &AuditAction::Login {
return Ok(None);
}
let recent = self.recent_events.read();
let mut recent_failures = recent
.iter()
.filter(|e| {
if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
uid == user_id
&& e.action() == &AuditAction::Login
&& e.outcome() == &AuditOutcome::Failure
&& (Utc::now() - e.timestamp()) < Duration::minutes(15)
} else {
false
}
})
.count();
if event.outcome() == &AuditOutcome::Failure {
recent_failures += 1;
}
if recent_failures >= 5 {
let score = (recent_failures as f64 / 10.0).min(1.0);
let factors = vec![format!(
"{} failed login attempts in 15 minutes",
recent_failures
)];
return Ok(Some((score, factors)));
}
Ok(None)
}
fn detect_unusual_access(
&self,
user_id: &str,
event: &AuditEvent,
) -> Result<Option<(f64, Vec<String>)>> {
let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
let profile = profile_ref.value();
if profile.event_count < self.config.read().min_baseline_events {
return Ok(None); }
let mut factors = Vec::new();
let mut anomaly_indicators = 0;
let hour = event.timestamp().hour();
if !profile.typical_hours.is_empty() && !profile.typical_hours.contains(&hour) {
factors.push(format!("Access at unusual hour: {hour}:00"));
anomaly_indicators += 1;
}
let action_count = profile
.typical_actions
.get(event.action())
.copied()
.unwrap_or(0);
if action_count == 0 && profile.event_count > 50 {
factors.push(format!("First time performing {:?}", event.action()));
anomaly_indicators += 1;
}
if anomaly_indicators > 0 {
let score = (f64::from(anomaly_indicators) / 2.0).min(1.0);
return Ok(Some((score, factors)));
}
}
Ok(None)
}
fn detect_privilege_escalation(
&self,
user_id: &str,
event: &AuditEvent,
) -> Result<Option<(f64, Vec<String>)>> {
let sensitive_actions = [AuditAction::TenantUpdated, AuditAction::RoleChanged];
if sensitive_actions.contains(event.action()) && event.outcome() == &AuditOutcome::Failure {
let recent = self.recent_events.read();
let recent_privilege_attempts = recent
.iter()
.filter(|e| {
if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
uid == user_id
&& sensitive_actions.contains(e.action())
&& (Utc::now() - e.timestamp()) < Duration::hours(1)
} else {
false
}
})
.count();
if recent_privilege_attempts >= 3 {
let score = 0.8;
let factors = vec![
format!(
"{} privilege escalation attempts in 1 hour",
recent_privilege_attempts
),
format!("Latest action: {:?}", event.action()),
];
return Ok(Some((score, factors)));
}
}
Ok(None)
}
fn detect_data_exfiltration(
&self,
user_id: &str,
event: &AuditEvent,
) -> Result<Option<(f64, Vec<String>)>> {
if event.action() != &AuditAction::EventQueried {
return Ok(None);
}
let recent = self.recent_events.read();
let recent_queries = recent
.iter()
.filter(|e| {
if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
uid == user_id
&& e.action() == &AuditAction::EventQueried
&& (Utc::now() - e.timestamp()) < Duration::hours(1)
} else {
false
}
})
.count();
let profile_key = format!("{}-{user_id}", event.tenant_id().as_str());
if let Some(profile_ref) = self.user_profiles.get(&profile_key) {
let profile = profile_ref.value();
if profile.event_count >= self.config.read().min_baseline_events {
if recent_queries as f64 > profile.avg_actions_per_hour * 5.0 {
let score = 0.75;
let factors = vec![
format!(
"{} queries in 1 hour (baseline: {:.0})",
recent_queries, profile.avg_actions_per_hour
),
"Potential data exfiltration pattern".to_string(),
];
return Ok(Some((score, factors)));
}
}
}
Ok(None)
}
fn detect_velocity_anomaly(
&self,
user_id: &str,
event: &AuditEvent,
) -> Result<Option<(f64, Vec<String>)>> {
let recent = self.recent_events.read();
let very_recent = recent
.iter()
.filter(|e| {
if let crate::domain::entities::Actor::User { user_id: uid, .. } = e.actor() {
uid == user_id && (Utc::now() - e.timestamp()) < Duration::seconds(10)
} else {
false
}
})
.count();
if very_recent >= 20 {
let score = 0.7;
let factors = vec![
format!("{very_recent} actions in 10 seconds"),
"Potential automated attack or compromised credentials".to_string(),
];
return Ok(Some((score, factors)));
}
Ok(None)
}
pub fn add_recent_event(&self, event: AuditEvent) {
let mut events = self.recent_events.write();
events.push(event);
let cutoff = Utc::now() - Duration::hours(24);
events.retain(|e| e.timestamp() > &cutoff);
if events.len() > 10000 {
events.drain(0..1000);
}
}
pub fn get_stats(&self) -> DetectionStats {
let recent = self.recent_events.read();
DetectionStats {
user_profiles_count: self.user_profiles.len(),
recent_events_count: recent.len(),
config: self.config.read().clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectionStats {
pub user_profiles_count: usize,
pub recent_events_count: usize,
pub config: AnomalyDetectionConfig,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{entities::Actor, value_objects::TenantId};
fn create_test_event(action: AuditAction, outcome: AuditOutcome, user_id: &str) -> AuditEvent {
let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
let actor = Actor::User {
user_id: user_id.to_string(),
username: "testuser".to_string(),
};
AuditEvent::new(tenant_id, action, actor, outcome)
}
fn create_system_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
let actor = Actor::System {
component: "test-service".to_string(),
};
AuditEvent::new(tenant_id, action, actor, outcome)
}
fn create_api_key_event(action: AuditAction, outcome: AuditOutcome) -> AuditEvent {
let tenant_id = TenantId::new("test-tenant".to_string()).unwrap();
let actor = Actor::ApiKey {
key_id: "key-123".to_string(),
key_name: "test-key".to_string(),
};
AuditEvent::new(tenant_id, action, actor, outcome)
}
#[test]
fn test_anomaly_detector_creation() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let stats = detector.get_stats();
assert_eq!(stats.user_profiles_count, 0);
assert_eq!(stats.recent_events_count, 0);
}
#[test]
fn test_normal_behavior_not_flagged() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous);
assert_eq!(result.recommended_action, RecommendedAction::Monitor);
}
#[test]
fn test_brute_force_detection() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..6 {
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
detector.add_recent_event(event.clone());
}
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.is_anomalous);
assert_eq!(result.anomaly_type, Some(AnomalyType::BruteForceAttack));
assert!(result.score >= 0.5);
}
#[test]
fn test_profile_building() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
detector.update_profile(&event).unwrap();
let stats = detector.get_stats();
assert_eq!(stats.user_profiles_count, 1);
}
#[test]
fn test_velocity_anomaly() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..25 {
let event =
create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
detector.add_recent_event(event.clone());
}
let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Success, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.is_anomalous);
assert_eq!(result.anomaly_type, Some(AnomalyType::VelocityAnomaly));
}
#[test]
fn test_disabled_detection() {
let config = AnomalyDetectionConfig {
enabled: false,
..Default::default()
};
let detector = AnomalyDetector::new(config);
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous);
}
#[test]
fn test_default_config() {
let config = AnomalyDetectionConfig::default();
assert!(config.enabled);
assert_eq!(config.sensitivity, 0.7);
assert_eq!(config.min_baseline_events, 100);
assert_eq!(config.analysis_window_hours, 24);
assert!(config.enable_brute_force_detection);
assert!(config.enable_unusual_access_detection);
assert!(config.enable_privilege_escalation_detection);
assert!(config.enable_data_exfiltration_detection);
assert!(config.enable_velocity_detection);
}
#[test]
fn test_config_serde() {
let config = AnomalyDetectionConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: AnomalyDetectionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.enabled, config.enabled);
assert_eq!(parsed.sensitivity, config.sensitivity);
}
#[test]
fn test_anomaly_type_equality() {
assert_eq!(AnomalyType::BruteForceAttack, AnomalyType::BruteForceAttack);
assert_ne!(AnomalyType::BruteForceAttack, AnomalyType::DataExfiltration);
}
#[test]
fn test_recommended_action_equality() {
assert_eq!(RecommendedAction::Monitor, RecommendedAction::Monitor);
assert_ne!(RecommendedAction::Monitor, RecommendedAction::Block);
}
#[test]
fn test_system_actor_not_flagged() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous);
assert_eq!(result.reason, "System actor");
}
#[test]
fn test_api_key_actor_analyzed() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous);
}
#[test]
fn test_update_profile_api_key() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_api_key_event(AuditAction::EventQueried, AuditOutcome::Success);
detector.update_profile(&event).unwrap();
let stats = detector.get_stats();
assert_eq!(stats.user_profiles_count, 1);
}
#[test]
fn test_update_profile_system() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
let event = create_system_event(AuditAction::EventIngested, AuditOutcome::Success);
detector.update_profile(&event).unwrap();
let stats = detector.get_stats();
assert_eq!(stats.user_profiles_count, 0);
}
#[test]
fn test_recent_events_pruning() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for i in 0..10050 {
let event = create_test_event(
AuditAction::EventQueried,
AuditOutcome::Success,
&format!("user{}", i % 100),
);
detector.add_recent_event(event);
}
let stats = detector.get_stats();
assert!(stats.recent_events_count <= 10050);
}
#[test]
fn test_detection_stats_serde() {
let stats = DetectionStats {
user_profiles_count: 10,
recent_events_count: 100,
config: AnomalyDetectionConfig::default(),
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("\"user_profiles_count\":10"));
assert!(json.contains("\"recent_events_count\":100"));
}
#[test]
fn test_anomaly_result_serde() {
let result = AnomalyResult {
is_anomalous: true,
score: 0.85,
anomaly_type: Some(AnomalyType::BruteForceAttack),
reason: "Test reason".to_string(),
recommended_action: RecommendedAction::Block,
factors: vec!["factor1".to_string(), "factor2".to_string()],
};
let json = serde_json::to_string(&result).unwrap();
let parsed: AnomalyResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.is_anomalous, result.is_anomalous);
assert_eq!(parsed.score, result.score);
assert_eq!(parsed.anomaly_type, result.anomaly_type);
}
#[test]
fn test_recommended_action_for_high_score() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..15 {
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
detector.add_recent_event(event.clone());
}
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.is_anomalous);
if result.score >= 0.9 {
assert_eq!(result.recommended_action, RecommendedAction::RevokeAccess);
} else if result.score >= 0.8 {
assert_eq!(result.recommended_action, RecommendedAction::Block);
}
}
#[test]
fn test_successful_login_after_failures() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..3 {
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
detector.add_recent_event(event);
}
let event = create_test_event(AuditAction::Login, AuditOutcome::Success, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
}
#[test]
fn test_privilege_escalation_detection() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..4 {
let event =
create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
detector.add_recent_event(event);
}
let event = create_test_event(AuditAction::TenantUpdated, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.is_anomalous);
assert_eq!(result.anomaly_type, Some(AnomalyType::PrivilegeEscalation));
}
#[test]
fn test_non_login_action_not_brute_force() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..10 {
let event =
create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
detector.add_recent_event(event);
}
let event = create_test_event(AuditAction::EventQueried, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
}
#[test]
fn test_multiple_users_independent() {
let detector = AnomalyDetector::new(AnomalyDetectionConfig::default());
for _ in 0..6 {
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
detector.add_recent_event(event);
}
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user2");
let result = detector.analyze_event(&event).unwrap();
assert!(!result.is_anomalous || result.anomaly_type != Some(AnomalyType::BruteForceAttack));
}
#[test]
fn test_anomaly_types_serde() {
let types = vec![
AnomalyType::BruteForceAttack,
AnomalyType::UnusualAccessPattern,
AnomalyType::PrivilegeEscalation,
AnomalyType::DataExfiltration,
AnomalyType::VelocityAnomaly,
AnomalyType::AccountCompromise,
AnomalyType::SuspiciousActivity,
];
for anomaly_type in types {
let json = serde_json::to_string(&anomaly_type).unwrap();
let parsed: AnomalyType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, anomaly_type);
}
}
#[test]
fn test_recommended_actions_serde() {
let actions = vec![
RecommendedAction::Monitor,
RecommendedAction::Alert,
RecommendedAction::Block,
RecommendedAction::RequireMFA,
RecommendedAction::RevokeAccess,
];
for action in actions {
let json = serde_json::to_string(&action).unwrap();
let parsed: RecommendedAction = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, action);
}
}
#[test]
fn test_disabled_individual_detectors() {
let config = AnomalyDetectionConfig {
enabled: true,
enable_brute_force_detection: false,
enable_velocity_detection: false,
..Default::default()
};
let detector = AnomalyDetector::new(config);
for _ in 0..10 {
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
detector.add_recent_event(event);
}
let event = create_test_event(AuditAction::Login, AuditOutcome::Failure, "user1");
let result = detector.analyze_event(&event).unwrap();
assert!(result.anomaly_type != Some(AnomalyType::BruteForceAttack));
}
}