use crate::audit_log::AuditLogEntry;
use crate::geo_analyzer::GeoAnomaly;
use chrono::{DateTime, Utc, Duration};
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Serialize)]
pub struct RiskScore {
pub user: String,
pub score: f64, pub factors: Vec<RiskFactor>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
pub struct RiskFactor {
pub factor_type: RiskFactorType,
pub weight: f64,
pub description: String,
}
#[derive(Debug, Serialize)]
pub enum RiskFactorType {
LocationAnomaly,
FailedAttempts,
UnusualTime,
UnusualDevice,
SuspiciousIP,
ResourceAccess,
}
pub struct RiskAnalyzer {
user_history: HashMap<String, UserBehavior>,
config: RiskConfig,
}
#[derive(Clone)]
pub struct RiskConfig {
pub location_weight: f64,
pub time_weight: f64,
pub attempts_weight: f64,
pub device_weight: f64,
pub ip_weight: f64,
pub resource_weight: f64,
pub high_risk_threshold: f64,
}
#[derive(Default)]
struct UserBehavior {
usual_locations: Vec<String>,
usual_access_times: Vec<u32>, usual_devices: Vec<String>,
access_patterns: Vec<AccessPattern>,
}
struct AccessPattern {
timestamp: DateTime<Utc>,
resource: String,
success: bool,
}
impl RiskAnalyzer {
pub fn new(config: RiskConfig) -> Self {
Self {
user_history: HashMap::new(),
config,
}
}
pub fn calculate_risk(&mut self, user: &str, context: &AccessContext) -> RiskScore {
let mut factors = Vec::new();
let mut total_score = 0.0;
if let Some(geo_anomaly) = &context.geo_anomaly {
factors.push(RiskFactor {
factor_type: RiskFactorType::LocationAnomaly,
weight: self.config.location_weight,
description: format!("Unusual location access from {}", geo_anomaly.current_location.country),
});
total_score += self.config.location_weight;
}
if self.is_unusual_time(user, context.timestamp) {
factors.push(RiskFactor {
factor_type: RiskFactorType::UnusualTime,
weight: self.config.time_weight,
description: "Access attempt at unusual time".to_string(),
});
total_score += self.config.time_weight;
}
if !self.is_known_device(user, &context.user_agent) {
factors.push(RiskFactor {
factor_type: RiskFactorType::UnusualDevice,
weight: self.config.device_weight,
description: "Access from unknown device".to_string(),
});
total_score += self.config.device_weight;
}
RiskScore {
user: user.to_string(),
score: total_score.min(1.0),
factors,
timestamp: Utc::now(),
}
}
pub fn update_behavior(&mut self, user: &str, entry: &AuditLogEntry) {
let behavior = self.user_history.entry(user.to_string()).or_default();
if let Some(location) = entry.metadata.ip_address.as_ref() {
if !behavior.usual_locations.contains(location) {
behavior.usual_locations.push(location.clone());
}
}
let hour = entry.timestamp.hour();
if !behavior.usual_access_times.contains(&hour) {
behavior.usual_access_times.push(hour);
}
if let Some(device) = entry.metadata.user_agent.as_ref() {
if !behavior.usual_devices.contains(device) {
behavior.usual_devices.push(device.clone());
}
}
behavior.access_patterns.push(AccessPattern {
timestamp: entry.timestamp,
resource: entry.resource.clone(),
success: matches!(entry.result, ActionResult::Success),
});
}
fn is_unusual_time(&self, user: &str, timestamp: DateTime<Utc>) -> bool {
if let Some(behavior) = self.user_history.get(user) {
!behavior.usual_access_times.contains(×tamp.hour())
} else {
false
}
}
fn is_known_device(&self, user: &str, device: &str) -> bool {
if let Some(behavior) = self.user_history.get(user) {
behavior.usual_devices.contains(device)
} else {
false
}
}
}
pub struct AccessContext {
pub timestamp: DateTime<Utc>,
pub ip_address: String,
pub user_agent: String,
pub geo_anomaly: Option<GeoAnomaly>,
pub recent_failed_attempts: u32,
}