mockforge_core/security/
risk_assessment.rs

1//! Risk Assessment System
2//!
3//! This module provides a comprehensive risk assessment framework for identifying,
4//! analyzing, evaluating, and treating information security risks.
5
6use crate::Error;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Risk category
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum RiskCategory {
16    /// Technical risks (vulnerabilities, system failures, data breaches)
17    Technical,
18    /// Operational risks (process failures, human error, access control)
19    Operational,
20    /// Compliance risks (regulatory violations, audit findings)
21    Compliance,
22    /// Business risks (reputation, financial, operational impact)
23    Business,
24}
25
26/// Likelihood scale (1-5)
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum Likelihood {
30    /// Rare (unlikely to occur)
31    Rare = 1,
32    /// Unlikely (possible but not expected)
33    Unlikely = 2,
34    /// Possible (could occur)
35    Possible = 3,
36    /// Likely (expected to occur)
37    Likely = 4,
38    /// Almost Certain (very likely to occur)
39    AlmostCertain = 5,
40}
41
42impl Likelihood {
43    /// Get numeric value
44    pub fn value(&self) -> u8 {
45        *self as u8
46    }
47}
48
49/// Impact scale (1-5)
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Impact {
53    /// Negligible (minimal impact)
54    Negligible = 1,
55    /// Low (minor impact)
56    Low = 2,
57    /// Medium (moderate impact)
58    Medium = 3,
59    /// High (significant impact)
60    High = 4,
61    /// Critical (severe impact)
62    Critical = 5,
63}
64
65impl Impact {
66    /// Get numeric value
67    pub fn value(&self) -> u8 {
68        *self as u8
69    }
70}
71
72/// Risk level based on score
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum RiskLevel {
76    /// Low risk (1-5): Monitor and review
77    Low,
78    /// Medium risk (6-11): Action required
79    Medium,
80    /// High risk (12-19): Urgent action required
81    High,
82    /// Critical risk (20-25): Immediate action required
83    Critical,
84}
85
86impl RiskLevel {
87    /// Calculate risk level from score
88    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/// Risk treatment option
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "lowercase")]
102pub enum TreatmentOption {
103    /// Avoid: Eliminate risk by not performing activity
104    Avoid,
105    /// Mitigate: Reduce risk through controls
106    Mitigate,
107    /// Transfer: Transfer risk (insurance, contracts)
108    Transfer,
109    /// Accept: Accept risk with monitoring
110    Accept,
111}
112
113/// Treatment status
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum TreatmentStatus {
117    /// Not started
118    NotStarted,
119    /// In progress
120    InProgress,
121    /// Completed
122    Completed,
123    /// On hold
124    OnHold,
125}
126
127/// Risk review frequency
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(rename_all = "lowercase")]
130pub enum RiskReviewFrequency {
131    /// Monthly reviews
132    Monthly,
133    /// Quarterly reviews
134    Quarterly,
135    /// Annual reviews
136    Annually,
137    /// Ad-hoc reviews
138    AdHoc,
139}
140
141/// Risk entry in the risk register
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct Risk {
144    /// Risk ID (e.g., "RISK-001")
145    pub risk_id: String,
146    /// Risk title
147    pub title: String,
148    /// Risk description
149    pub description: String,
150    /// Risk category
151    pub category: RiskCategory,
152    /// Risk subcategory (optional)
153    pub subcategory: Option<String>,
154    /// Likelihood (1-5)
155    pub likelihood: Likelihood,
156    /// Impact (1-5)
157    pub impact: Impact,
158    /// Risk score (likelihood × impact, 1-25)
159    pub risk_score: u8,
160    /// Risk level
161    pub risk_level: RiskLevel,
162    /// Threat description
163    pub threat: Option<String>,
164    /// Vulnerability description
165    pub vulnerability: Option<String>,
166    /// Affected asset
167    pub asset: Option<String>,
168    /// Existing controls
169    pub existing_controls: Vec<String>,
170    /// Treatment option
171    pub treatment_option: TreatmentOption,
172    /// Treatment plan
173    pub treatment_plan: Vec<String>,
174    /// Treatment owner
175    pub treatment_owner: Option<String>,
176    /// Treatment deadline
177    pub treatment_deadline: Option<DateTime<Utc>>,
178    /// Treatment status
179    pub treatment_status: TreatmentStatus,
180    /// Residual likelihood (after treatment)
181    pub residual_likelihood: Option<Likelihood>,
182    /// Residual impact (after treatment)
183    pub residual_impact: Option<Impact>,
184    /// Residual risk score (after treatment)
185    pub residual_risk_score: Option<u8>,
186    /// Residual risk level (after treatment)
187    pub residual_risk_level: Option<RiskLevel>,
188    /// Last reviewed date
189    pub last_reviewed: Option<DateTime<Utc>>,
190    /// Next review date
191    pub next_review: Option<DateTime<Utc>>,
192    /// Review frequency
193    pub review_frequency: RiskReviewFrequency,
194    /// Compliance requirements
195    pub compliance_requirements: Vec<String>,
196    /// Created date
197    pub created_at: DateTime<Utc>,
198    /// Updated date
199    pub updated_at: DateTime<Utc>,
200    /// Created by user ID
201    pub created_by: Uuid,
202}
203
204impl Risk {
205    /// Create a new risk
206    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    /// Recalculate risk score and level
252    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    /// Calculate next review date based on frequency
263    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), // Default to quarterly
270        };
271        self.next_review = Some(next);
272    }
273}
274
275/// Risk register summary
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RiskSummary {
278    /// Total risks
279    pub total_risks: u32,
280    /// Critical risks
281    pub critical: u32,
282    /// High risks
283    pub high: u32,
284    /// Medium risks
285    pub medium: u32,
286    /// Low risks
287    pub low: u32,
288    /// Risks by category
289    pub by_category: HashMap<RiskCategory, u32>,
290    /// Risks by treatment status
291    pub by_treatment_status: HashMap<TreatmentStatus, u32>,
292}
293
294/// Risk assessment configuration
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct RiskAssessmentConfig {
297    /// Whether risk assessment is enabled
298    pub enabled: bool,
299    /// Default review frequency
300    pub default_review_frequency: RiskReviewFrequency,
301    /// Risk tolerance thresholds
302    pub risk_tolerance: RiskTolerance,
303}
304
305/// Risk tolerance thresholds
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct RiskTolerance {
308    /// Maximum acceptable risk score
309    pub max_acceptable_score: u8,
310    /// Require treatment for risks above this score
311    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, // Low risks acceptable
321                require_treatment_above: 11, // Medium and above require treatment
322            },
323        }
324    }
325}
326
327/// Risk assessment engine
328pub struct RiskAssessmentEngine {
329    config: RiskAssessmentConfig,
330    /// Risk register
331    risks: std::sync::Arc<tokio::sync::RwLock<HashMap<String, Risk>>>,
332    /// Risk ID counter
333    risk_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
334}
335
336impl RiskAssessmentEngine {
337    /// Create a new risk assessment engine
338    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    /// Generate next risk ID
347    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    /// Create a new risk
354    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    /// Get risk by ID
375    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    /// Get all risks
381    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    /// Get risks by level
387    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    /// Get risks by category
397    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    /// Get risks by treatment status
407    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    /// Update risk
417    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    /// Update risk likelihood and impact
431    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    /// Update treatment plan
454    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    /// Update treatment status
476    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    /// Set residual risk
492    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    /// Review risk
511    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; // TODO: Store reviewer
518            Ok(())
519        } else {
520            Err(Error::Generic("Risk not found".to_string()))
521        }
522    }
523
524    /// Get risk summary
525    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    /// Get risks due for review
555    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); // 3 * 4
593        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}