1use crate::Error;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum RiskCategory {
16 Technical,
18 Operational,
20 Compliance,
22 Business,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum Likelihood {
30 Rare = 1,
32 Unlikely = 2,
34 Possible = 3,
36 Likely = 4,
38 AlmostCertain = 5,
40}
41
42impl Likelihood {
43 pub fn value(&self) -> u8 {
45 *self as u8
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Impact {
53 Negligible = 1,
55 Low = 2,
57 Medium = 3,
59 High = 4,
61 Critical = 5,
63}
64
65impl Impact {
66 pub fn value(&self) -> u8 {
68 *self as u8
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum RiskLevel {
76 Low,
78 Medium,
80 High,
82 Critical,
84}
85
86impl RiskLevel {
87 pub fn from_score(score: u8) -> Self {
89 match score {
90 1..=5 => RiskLevel::Low,
91 6..=11 => RiskLevel::Medium,
92 12..=19 => RiskLevel::High,
93 20..=25 => RiskLevel::Critical,
94 _ => RiskLevel::Low,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "lowercase")]
102pub enum TreatmentOption {
103 Avoid,
105 Mitigate,
107 Transfer,
109 Accept,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum TreatmentStatus {
117 NotStarted,
119 InProgress,
121 Completed,
123 OnHold,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(rename_all = "lowercase")]
130pub enum RiskReviewFrequency {
131 Monthly,
133 Quarterly,
135 Annually,
137 AdHoc,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct Risk {
144 pub risk_id: String,
146 pub title: String,
148 pub description: String,
150 pub category: RiskCategory,
152 pub subcategory: Option<String>,
154 pub likelihood: Likelihood,
156 pub impact: Impact,
158 pub risk_score: u8,
160 pub risk_level: RiskLevel,
162 pub threat: Option<String>,
164 pub vulnerability: Option<String>,
166 pub asset: Option<String>,
168 pub existing_controls: Vec<String>,
170 pub treatment_option: TreatmentOption,
172 pub treatment_plan: Vec<String>,
174 pub treatment_owner: Option<String>,
176 pub treatment_deadline: Option<DateTime<Utc>>,
178 pub treatment_status: TreatmentStatus,
180 pub residual_likelihood: Option<Likelihood>,
182 pub residual_impact: Option<Impact>,
184 pub residual_risk_score: Option<u8>,
186 pub residual_risk_level: Option<RiskLevel>,
188 pub last_reviewed: Option<DateTime<Utc>>,
190 pub next_review: Option<DateTime<Utc>>,
192 pub review_frequency: RiskReviewFrequency,
194 pub compliance_requirements: Vec<String>,
196 pub created_at: DateTime<Utc>,
198 pub updated_at: DateTime<Utc>,
200 pub created_by: Uuid,
202}
203
204impl Risk {
205 pub fn new(
207 risk_id: String,
208 title: String,
209 description: String,
210 category: RiskCategory,
211 likelihood: Likelihood,
212 impact: Impact,
213 created_by: Uuid,
214 ) -> Self {
215 let risk_score = likelihood.value() * impact.value();
216 let risk_level = RiskLevel::from_score(risk_score);
217
218 Self {
219 risk_id,
220 title,
221 description,
222 category,
223 subcategory: None,
224 likelihood,
225 impact,
226 risk_score,
227 risk_level,
228 threat: None,
229 vulnerability: None,
230 asset: None,
231 existing_controls: Vec::new(),
232 treatment_option: TreatmentOption::Accept,
233 treatment_plan: Vec::new(),
234 treatment_owner: None,
235 treatment_deadline: None,
236 treatment_status: TreatmentStatus::NotStarted,
237 residual_likelihood: None,
238 residual_impact: None,
239 residual_risk_score: None,
240 residual_risk_level: None,
241 last_reviewed: None,
242 next_review: None,
243 review_frequency: RiskReviewFrequency::Quarterly,
244 compliance_requirements: Vec::new(),
245 created_at: Utc::now(),
246 updated_at: Utc::now(),
247 created_by,
248 }
249 }
250
251 pub fn recalculate(&mut self) {
253 self.risk_score = self.likelihood.value() * self.impact.value();
254 self.risk_level = RiskLevel::from_score(self.risk_score);
255
256 if let (Some(res_likelihood), Some(res_impact)) = (self.residual_likelihood, self.residual_impact) {
257 self.residual_risk_score = Some(res_likelihood.value() * res_impact.value());
258 self.residual_risk_level = self.residual_risk_score.map(RiskLevel::from_score);
259 }
260 }
261
262 pub fn calculate_next_review(&mut self) {
264 let now = Utc::now();
265 let next = match self.review_frequency {
266 RiskReviewFrequency::Monthly => now + chrono::Duration::days(30),
267 RiskReviewFrequency::Quarterly => now + chrono::Duration::days(90),
268 RiskReviewFrequency::Annually => now + chrono::Duration::days(365),
269 RiskReviewFrequency::AdHoc => now + chrono::Duration::days(90), };
271 self.next_review = Some(next);
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RiskSummary {
278 pub total_risks: u32,
280 pub critical: u32,
282 pub high: u32,
284 pub medium: u32,
286 pub low: u32,
288 pub by_category: HashMap<RiskCategory, u32>,
290 pub by_treatment_status: HashMap<TreatmentStatus, u32>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct RiskAssessmentConfig {
297 pub enabled: bool,
299 pub default_review_frequency: RiskReviewFrequency,
301 pub risk_tolerance: RiskTolerance,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct RiskTolerance {
308 pub max_acceptable_score: u8,
310 pub require_treatment_above: u8,
312}
313
314impl Default for RiskAssessmentConfig {
315 fn default() -> Self {
316 Self {
317 enabled: true,
318 default_review_frequency: RiskReviewFrequency::Quarterly,
319 risk_tolerance: RiskTolerance {
320 max_acceptable_score: 5, require_treatment_above: 11, },
323 }
324 }
325}
326
327pub struct RiskAssessmentEngine {
329 config: RiskAssessmentConfig,
330 risks: std::sync::Arc<tokio::sync::RwLock<HashMap<String, Risk>>>,
332 risk_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
334}
335
336impl RiskAssessmentEngine {
337 pub fn new(config: RiskAssessmentConfig) -> Self {
339 Self {
340 config,
341 risks: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
342 risk_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
343 }
344 }
345
346 async fn generate_risk_id(&self) -> String {
348 let mut counter = self.risk_id_counter.write().await;
349 *counter += 1;
350 format!("RISK-{:03}", *counter)
351 }
352
353 pub async fn create_risk(
355 &self,
356 title: String,
357 description: String,
358 category: RiskCategory,
359 likelihood: Likelihood,
360 impact: Impact,
361 created_by: Uuid,
362 ) -> Result<Risk, Error> {
363 let risk_id = self.generate_risk_id().await;
364 let mut risk = Risk::new(risk_id.clone(), title, description, category, likelihood, impact, created_by);
365 risk.review_frequency = self.config.default_review_frequency;
366 risk.calculate_next_review();
367
368 let mut risks = self.risks.write().await;
369 risks.insert(risk_id, risk.clone());
370
371 Ok(risk)
372 }
373
374 pub async fn get_risk(&self, risk_id: &str) -> Result<Option<Risk>, Error> {
376 let risks = self.risks.read().await;
377 Ok(risks.get(risk_id).cloned())
378 }
379
380 pub async fn get_all_risks(&self) -> Result<Vec<Risk>, Error> {
382 let risks = self.risks.read().await;
383 Ok(risks.values().cloned().collect())
384 }
385
386 pub async fn get_risks_by_level(&self, level: RiskLevel) -> Result<Vec<Risk>, Error> {
388 let risks = self.risks.read().await;
389 Ok(risks
390 .values()
391 .filter(|r| r.risk_level == level)
392 .cloned()
393 .collect())
394 }
395
396 pub async fn get_risks_by_category(&self, category: RiskCategory) -> Result<Vec<Risk>, Error> {
398 let risks = self.risks.read().await;
399 Ok(risks
400 .values()
401 .filter(|r| r.category == category)
402 .cloned()
403 .collect())
404 }
405
406 pub async fn get_risks_by_treatment_status(&self, status: TreatmentStatus) -> Result<Vec<Risk>, Error> {
408 let risks = self.risks.read().await;
409 Ok(risks
410 .values()
411 .filter(|r| r.treatment_status == status)
412 .cloned()
413 .collect())
414 }
415
416 pub async fn update_risk(&self, risk_id: &str, mut risk: Risk) -> Result<(), Error> {
418 risk.recalculate();
419 risk.updated_at = Utc::now();
420
421 let mut risks = self.risks.write().await;
422 if risks.contains_key(risk_id) {
423 risks.insert(risk_id.to_string(), risk);
424 Ok(())
425 } else {
426 Err(Error::Generic("Risk not found".to_string()))
427 }
428 }
429
430 pub async fn update_risk_assessment(
432 &self,
433 risk_id: &str,
434 likelihood: Option<Likelihood>,
435 impact: Option<Impact>,
436 ) -> Result<(), Error> {
437 let mut risks = self.risks.write().await;
438 if let Some(risk) = risks.get_mut(risk_id) {
439 if let Some(l) = likelihood {
440 risk.likelihood = l;
441 }
442 if let Some(i) = impact {
443 risk.impact = i;
444 }
445 risk.recalculate();
446 risk.updated_at = Utc::now();
447 Ok(())
448 } else {
449 Err(Error::Generic("Risk not found".to_string()))
450 }
451 }
452
453 pub async fn update_treatment_plan(
455 &self,
456 risk_id: &str,
457 treatment_option: TreatmentOption,
458 treatment_plan: Vec<String>,
459 treatment_owner: Option<String>,
460 treatment_deadline: Option<DateTime<Utc>>,
461 ) -> Result<(), Error> {
462 let mut risks = self.risks.write().await;
463 if let Some(risk) = risks.get_mut(risk_id) {
464 risk.treatment_option = treatment_option;
465 risk.treatment_plan = treatment_plan;
466 risk.treatment_owner = treatment_owner;
467 risk.treatment_deadline = treatment_deadline;
468 risk.updated_at = Utc::now();
469 Ok(())
470 } else {
471 Err(Error::Generic("Risk not found".to_string()))
472 }
473 }
474
475 pub async fn update_treatment_status(
477 &self,
478 risk_id: &str,
479 status: TreatmentStatus,
480 ) -> Result<(), Error> {
481 let mut risks = self.risks.write().await;
482 if let Some(risk) = risks.get_mut(risk_id) {
483 risk.treatment_status = status;
484 risk.updated_at = Utc::now();
485 Ok(())
486 } else {
487 Err(Error::Generic("Risk not found".to_string()))
488 }
489 }
490
491 pub async fn set_residual_risk(
493 &self,
494 risk_id: &str,
495 residual_likelihood: Likelihood,
496 residual_impact: Impact,
497 ) -> Result<(), Error> {
498 let mut risks = self.risks.write().await;
499 if let Some(risk) = risks.get_mut(risk_id) {
500 risk.residual_likelihood = Some(residual_likelihood);
501 risk.residual_impact = Some(residual_impact);
502 risk.recalculate();
503 risk.updated_at = Utc::now();
504 Ok(())
505 } else {
506 Err(Error::Generic("Risk not found".to_string()))
507 }
508 }
509
510 pub async fn review_risk(&self, risk_id: &str, reviewed_by: Uuid) -> Result<(), Error> {
512 let mut risks = self.risks.write().await;
513 if let Some(risk) = risks.get_mut(risk_id) {
514 risk.last_reviewed = Some(Utc::now());
515 risk.calculate_next_review();
516 risk.updated_at = Utc::now();
517 let _ = reviewed_by; Ok(())
519 } else {
520 Err(Error::Generic("Risk not found".to_string()))
521 }
522 }
523
524 pub async fn get_risk_summary(&self) -> Result<RiskSummary, Error> {
526 let risks = self.risks.read().await;
527
528 let mut summary = RiskSummary {
529 total_risks: risks.len() as u32,
530 critical: 0,
531 high: 0,
532 medium: 0,
533 low: 0,
534 by_category: HashMap::new(),
535 by_treatment_status: HashMap::new(),
536 };
537
538 for risk in risks.values() {
539 match risk.risk_level {
540 RiskLevel::Critical => summary.critical += 1,
541 RiskLevel::High => summary.high += 1,
542 RiskLevel::Medium => summary.medium += 1,
543 RiskLevel::Low => summary.low += 1,
544 }
545
546 *summary.by_category.entry(risk.category).or_insert(0) += 1;
547 let count = summary.by_treatment_status.entry(risk.treatment_status).or_insert(0);
548 *count += 1;
549 }
550
551 Ok(summary)
552 }
553
554 pub async fn get_risks_due_for_review(&self) -> Result<Vec<Risk>, Error> {
556 let risks = self.risks.read().await;
557 let now = Utc::now();
558
559 Ok(risks
560 .values()
561 .filter(|r| {
562 r.next_review
563 .map(|next| next <= now)
564 .unwrap_or(false)
565 })
566 .cloned()
567 .collect())
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[tokio::test]
576 async fn test_risk_creation() {
577 let config = RiskAssessmentConfig::default();
578 let engine = RiskAssessmentEngine::new(config);
579
580 let risk = engine
581 .create_risk(
582 "Test Risk".to_string(),
583 "Test description".to_string(),
584 RiskCategory::Technical,
585 Likelihood::Possible,
586 Impact::High,
587 Uuid::new_v4(),
588 )
589 .await
590 .unwrap();
591
592 assert_eq!(risk.risk_score, 12); assert_eq!(risk.risk_level, RiskLevel::High);
594 }
595
596 #[test]
597 fn test_risk_level_calculation() {
598 assert_eq!(RiskLevel::from_score(3), RiskLevel::Low);
599 assert_eq!(RiskLevel::from_score(9), RiskLevel::Medium);
600 assert_eq!(RiskLevel::from_score(15), RiskLevel::High);
601 assert_eq!(RiskLevel::from_score(22), RiskLevel::Critical);
602 }
603}