mockforge_http/auth/
risk_engine.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11pub use mockforge_foundation::auth_risk_types::{RiskAction, RiskAssessment, RiskFactor};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct RiskEngineConfig {
16 pub mfa_threshold: f64,
18 pub device_challenge_threshold: f64,
20 pub blocked_login_threshold: f64,
22 pub risk_factors: HashMap<String, f64>,
24 pub risk_rules: Vec<RiskRule>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RiskRule {
31 pub condition: String,
33 pub action: RiskAction,
35}
36
37impl Default for RiskEngineConfig {
38 fn default() -> Self {
39 let mut risk_factors = HashMap::new();
40 risk_factors.insert("new_device".to_string(), 0.3);
41 risk_factors.insert("unusual_location".to_string(), 0.4);
42 risk_factors.insert("suspicious_activity".to_string(), 0.5);
43
44 let risk_rules = vec![
45 RiskRule {
46 condition: "risk_score > 0.9".to_string(),
47 action: RiskAction::Block,
48 },
49 RiskRule {
50 condition: "risk_score > 0.7".to_string(),
51 action: RiskAction::RequireMfa,
52 },
53 RiskRule {
54 condition: "risk_score > 0.5".to_string(),
55 action: RiskAction::DeviceChallenge,
56 },
57 ];
58
59 Self {
60 mfa_threshold: 0.7,
61 device_challenge_threshold: 0.5,
62 blocked_login_threshold: 0.9,
63 risk_factors,
64 risk_rules,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct RiskEngine {
72 pub config: RiskEngineConfig,
74 pub simulated_risks: Arc<RwLock<HashMap<String, Option<f64>>>>,
76 pub simulated_factors: Arc<RwLock<HashMap<String, HashMap<String, f64>>>>,
78}
79
80impl RiskEngine {
81 pub fn new(config: RiskEngineConfig) -> Self {
83 Self {
84 config,
85 simulated_risks: Arc::new(RwLock::new(HashMap::new())),
86 simulated_factors: Arc::new(RwLock::new(HashMap::new())),
87 }
88 }
89
90 pub async fn assess_risk(
92 &self,
93 user_id: &str,
94 risk_factors: &HashMap<String, f64>,
95 ) -> RiskAssessment {
96 let simulated_risk = {
98 let risks = self.simulated_risks.read().await;
99 risks.get(user_id).copied().flatten()
100 };
101
102 if let Some(risk_score) = simulated_risk {
103 return self.create_assessment_from_score(risk_score);
104 }
105
106 let factors_to_use = {
108 let simulated = self.simulated_factors.read().await;
109 if let Some(simulated_factors) = simulated.get(user_id) {
110 simulated_factors.clone()
111 } else {
112 risk_factors.clone()
113 }
114 };
115
116 let mut risk_factors_vec = Vec::new();
118 let mut total_score = 0.0;
119
120 for (name, value) in factors_to_use {
121 let weight = self.config.risk_factors.get(&name).copied().unwrap_or(0.0);
122 let contribution = weight * value;
123 total_score += contribution;
124
125 risk_factors_vec.push(RiskFactor {
126 name: name.clone(),
127 weight,
128 value,
129 contribution,
130 });
131 }
132
133 let risk_score = total_score.clamp(0.0, 1.0);
135
136 let recommended_action = self.determine_action(risk_score);
138
139 RiskAssessment {
140 risk_score,
141 risk_factors: risk_factors_vec,
142 recommended_action,
143 }
144 }
145
146 fn create_assessment_from_score(&self, risk_score: f64) -> RiskAssessment {
148 let recommended_action = self.determine_action(risk_score);
149
150 RiskAssessment {
151 risk_score,
152 risk_factors: vec![],
153 recommended_action,
154 }
155 }
156
157 fn determine_action(&self, risk_score: f64) -> RiskAction {
159 for rule in &self.config.risk_rules {
161 if self.evaluate_condition(&rule.condition, risk_score) {
162 return rule.action.clone();
163 }
164 }
165
166 if risk_score >= self.config.blocked_login_threshold {
168 RiskAction::Block
169 } else if risk_score >= self.config.mfa_threshold {
170 RiskAction::RequireMfa
171 } else if risk_score >= self.config.device_challenge_threshold {
172 RiskAction::DeviceChallenge
173 } else {
174 RiskAction::Allow
175 }
176 }
177
178 fn evaluate_condition(&self, condition: &str, risk_score: f64) -> bool {
180 if condition.contains(">=") {
183 let parts: Vec<&str> = condition.split(">=").collect();
184 if parts.len() == 2 {
185 if let Ok(threshold) = parts[1].trim().parse::<f64>() {
186 return risk_score >= threshold;
187 }
188 }
189 } else if condition.contains("<=") {
190 let parts: Vec<&str> = condition.split("<=").collect();
191 if parts.len() == 2 {
192 if let Ok(threshold) = parts[1].trim().parse::<f64>() {
193 return risk_score <= threshold;
194 }
195 }
196 } else if condition.contains("==") {
197 let parts: Vec<&str> = condition.split("==").collect();
198 if parts.len() == 2 {
199 if let Ok(threshold) = parts[1].trim().parse::<f64>() {
200 return (risk_score - threshold).abs() < 0.001;
201 }
202 }
203 } else if condition.contains('>') {
204 let parts: Vec<&str> = condition.split('>').collect();
205 if parts.len() == 2 {
206 if let Ok(threshold) = parts[1].trim().parse::<f64>() {
207 return risk_score > threshold;
208 }
209 }
210 } else if condition.contains('<') {
211 let parts: Vec<&str> = condition.split('<').collect();
212 if parts.len() == 2 {
213 if let Ok(threshold) = parts[1].trim().parse::<f64>() {
214 return risk_score < threshold;
215 }
216 }
217 }
218
219 false
220 }
221
222 pub async fn set_simulated_risk(&self, user_id: String, risk_score: Option<f64>) {
224 let mut risks = self.simulated_risks.write().await;
225 if let Some(score) = risk_score {
226 risks.insert(user_id, Some(score));
227 } else {
228 risks.remove(&user_id);
229 }
230 }
231
232 pub async fn set_simulated_factors(&self, user_id: String, factors: HashMap<String, f64>) {
234 let mut simulated = self.simulated_factors.write().await;
235 simulated.insert(user_id, factors);
236 }
237
238 pub async fn clear_simulated_risk(&self, user_id: &str) {
240 let mut risks = self.simulated_risks.write().await;
241 risks.remove(user_id);
242 let mut factors = self.simulated_factors.write().await;
243 factors.remove(user_id);
244 }
245}
246
247impl Default for RiskEngine {
248 fn default() -> Self {
249 Self::new(RiskEngineConfig::default())
250 }
251}