rust_threat_detector/
incident_response.rs

1//! Automated Incident Response Module v2.0
2//!
3//! Provides automated response playbooks, incident tracking, and
4//! remediation workflows for detected threats.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11use crate::{ThreatAlert, ThreatCategory, ThreatSeverity};
12
13/// Incident status
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum IncidentStatus {
16    New,
17    Acknowledged,
18    Investigating,
19    Containing,
20    Eradicating,
21    Recovering,
22    Resolved,
23    Closed,
24}
25
26impl IncidentStatus {
27    /// Get next expected status
28    pub fn next(&self) -> Option<IncidentStatus> {
29        match self {
30            IncidentStatus::New => Some(IncidentStatus::Acknowledged),
31            IncidentStatus::Acknowledged => Some(IncidentStatus::Investigating),
32            IncidentStatus::Investigating => Some(IncidentStatus::Containing),
33            IncidentStatus::Containing => Some(IncidentStatus::Eradicating),
34            IncidentStatus::Eradicating => Some(IncidentStatus::Recovering),
35            IncidentStatus::Recovering => Some(IncidentStatus::Resolved),
36            IncidentStatus::Resolved => Some(IncidentStatus::Closed),
37            IncidentStatus::Closed => None,
38        }
39    }
40
41    /// Check if incident is active
42    pub fn is_active(&self) -> bool {
43        !matches!(self, IncidentStatus::Resolved | IncidentStatus::Closed)
44    }
45}
46
47/// Response action types
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub enum ResponseAction {
50    BlockIP { ip: String, duration_hours: u32 },
51    DisableAccount { account: String },
52    IsolateHost { hostname: String },
53    TerminateSession { session_id: String },
54    NotifyTeam { team: String, message: String },
55    CreateTicket { system: String, priority: String },
56    CollectEvidence { sources: Vec<String> },
57    RunScan { scan_type: String, target: String },
58    ResetCredentials { account: String },
59    EnableMFA { account: String },
60    UpdateFirewall { rule: String },
61    EscalateToSOC { priority: String },
62    Custom { name: String, parameters: HashMap<String, String> },
63}
64
65impl ResponseAction {
66    /// Get action name
67    pub fn name(&self) -> &str {
68        match self {
69            ResponseAction::BlockIP { .. } => "Block IP Address",
70            ResponseAction::DisableAccount { .. } => "Disable Account",
71            ResponseAction::IsolateHost { .. } => "Isolate Host",
72            ResponseAction::TerminateSession { .. } => "Terminate Session",
73            ResponseAction::NotifyTeam { .. } => "Notify Team",
74            ResponseAction::CreateTicket { .. } => "Create Ticket",
75            ResponseAction::CollectEvidence { .. } => "Collect Evidence",
76            ResponseAction::RunScan { .. } => "Run Security Scan",
77            ResponseAction::ResetCredentials { .. } => "Reset Credentials",
78            ResponseAction::EnableMFA { .. } => "Enable MFA",
79            ResponseAction::UpdateFirewall { .. } => "Update Firewall",
80            ResponseAction::EscalateToSOC { .. } => "Escalate to SOC",
81            ResponseAction::Custom { name, .. } => name,
82        }
83    }
84
85    /// Check if action is reversible
86    pub fn is_reversible(&self) -> bool {
87        matches!(
88            self,
89            ResponseAction::BlockIP { .. }
90            | ResponseAction::DisableAccount { .. }
91            | ResponseAction::IsolateHost { .. }
92        )
93    }
94}
95
96/// Response action result
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ActionResult {
99    pub action: ResponseAction,
100    pub success: bool,
101    pub executed_at: DateTime<Utc>,
102    pub message: String,
103    pub execution_time_ms: u64,
104}
105
106/// Response playbook
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Playbook {
109    pub id: String,
110    pub name: String,
111    pub description: String,
112    pub threat_categories: Vec<ThreatCategory>,
113    pub min_severity: ThreatSeverity,
114    pub actions: Vec<PlaybookAction>,
115    pub enabled: bool,
116    pub requires_approval: bool,
117}
118
119/// Playbook action with conditions
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct PlaybookAction {
122    pub action: ResponseAction,
123    pub order: u32,
124    pub condition: Option<String>,
125    pub timeout_seconds: u32,
126    pub on_failure: FailureAction,
127}
128
129/// What to do on action failure
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub enum FailureAction {
132    Continue,
133    Stop,
134    Retry { max_attempts: u32 },
135    Escalate,
136}
137
138/// Security incident
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Incident {
141    pub id: String,
142    pub title: String,
143    pub description: String,
144    pub status: IncidentStatus,
145    pub severity: ThreatSeverity,
146    pub category: ThreatCategory,
147    pub created_at: DateTime<Utc>,
148    pub updated_at: DateTime<Utc>,
149    pub acknowledged_at: Option<DateTime<Utc>>,
150    pub resolved_at: Option<DateTime<Utc>>,
151    pub assigned_to: Option<String>,
152    pub related_alerts: Vec<String>,
153    pub affected_assets: Vec<String>,
154    pub actions_taken: Vec<ActionResult>,
155    pub notes: Vec<IncidentNote>,
156    pub timeline: Vec<TimelineEntry>,
157    pub metrics: IncidentMetrics,
158}
159
160/// Incident note
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct IncidentNote {
163    pub id: String,
164    pub author: String,
165    pub content: String,
166    pub created_at: DateTime<Utc>,
167}
168
169/// Timeline entry
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TimelineEntry {
172    pub timestamp: DateTime<Utc>,
173    pub event_type: String,
174    pub description: String,
175    pub actor: String,
176}
177
178/// Incident metrics
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct IncidentMetrics {
181    pub time_to_detect_seconds: i64,
182    pub time_to_acknowledge_seconds: Option<i64>,
183    pub time_to_contain_seconds: Option<i64>,
184    pub time_to_resolve_seconds: Option<i64>,
185    pub total_actions: usize,
186    pub successful_actions: usize,
187}
188
189impl Incident {
190    /// Create new incident from alert
191    pub fn from_alert(alert: &ThreatAlert) -> Self {
192        let id = Uuid::new_v4().to_string();
193        let now = Utc::now();
194
195        Self {
196            id: id.clone(),
197            title: format!("{:?}: {}", alert.category, alert.description),
198            description: alert.description.clone(),
199            status: IncidentStatus::New,
200            severity: alert.severity,
201            category: alert.category.clone(),
202            created_at: now,
203            updated_at: now,
204            acknowledged_at: None,
205            resolved_at: None,
206            assigned_to: None,
207            related_alerts: vec![alert.alert_id.clone()],
208            affected_assets: alert.indicators.clone(),
209            actions_taken: Vec::new(),
210            notes: Vec::new(),
211            timeline: vec![TimelineEntry {
212                timestamp: now,
213                event_type: "Created".to_string(),
214                description: "Incident created from alert".to_string(),
215                actor: "System".to_string(),
216            }],
217            metrics: IncidentMetrics {
218                time_to_detect_seconds: 0,
219                time_to_acknowledge_seconds: None,
220                time_to_contain_seconds: None,
221                time_to_resolve_seconds: None,
222                total_actions: 0,
223                successful_actions: 0,
224            },
225        }
226    }
227
228    /// Update incident status
229    pub fn update_status(&mut self, new_status: IncidentStatus, actor: &str) {
230        let old_status = self.status;
231        self.status = new_status;
232        self.updated_at = Utc::now();
233
234        self.timeline.push(TimelineEntry {
235            timestamp: Utc::now(),
236            event_type: "Status Change".to_string(),
237            description: format!("{:?} -> {:?}", old_status, new_status),
238            actor: actor.to_string(),
239        });
240
241        // Update metrics
242        match new_status {
243            IncidentStatus::Acknowledged => {
244                self.acknowledged_at = Some(Utc::now());
245                self.metrics.time_to_acknowledge_seconds = Some(
246                    (Utc::now() - self.created_at).num_seconds()
247                );
248            }
249            IncidentStatus::Containing => {
250                self.metrics.time_to_contain_seconds = Some(
251                    (Utc::now() - self.created_at).num_seconds()
252                );
253            }
254            IncidentStatus::Resolved => {
255                self.resolved_at = Some(Utc::now());
256                self.metrics.time_to_resolve_seconds = Some(
257                    (Utc::now() - self.created_at).num_seconds()
258                );
259            }
260            _ => {}
261        }
262    }
263
264    /// Add action result
265    pub fn add_action_result(&mut self, result: ActionResult) {
266        self.metrics.total_actions += 1;
267        if result.success {
268            self.metrics.successful_actions += 1;
269        }
270
271        self.timeline.push(TimelineEntry {
272            timestamp: result.executed_at,
273            event_type: "Action Executed".to_string(),
274            description: format!("{}: {}", result.action.name(), result.message),
275            actor: "System".to_string(),
276        });
277
278        self.actions_taken.push(result);
279        self.updated_at = Utc::now();
280    }
281
282    /// Add note to incident
283    pub fn add_note(&mut self, author: &str, content: &str) {
284        self.notes.push(IncidentNote {
285            id: Uuid::new_v4().to_string(),
286            author: author.to_string(),
287            content: content.to_string(),
288            created_at: Utc::now(),
289        });
290        self.updated_at = Utc::now();
291    }
292
293    /// Calculate incident duration
294    pub fn duration(&self) -> Duration {
295        let end_time = self.resolved_at.unwrap_or_else(Utc::now);
296        end_time - self.created_at
297    }
298
299    /// Check if incident is overdue
300    pub fn is_overdue(&self, sla_hours: i64) -> bool {
301        if !self.status.is_active() {
302            return false;
303        }
304        let elapsed = Utc::now() - self.created_at;
305        elapsed.num_hours() > sla_hours
306    }
307}
308
309/// Incident response manager
310pub struct IncidentResponseManager {
311    incidents: HashMap<String, Incident>,
312    playbooks: Vec<Playbook>,
313    auto_response_enabled: bool,
314}
315
316impl IncidentResponseManager {
317    /// Create new incident response manager
318    pub fn new() -> Self {
319        let mut manager = Self {
320            incidents: HashMap::new(),
321            playbooks: Vec::new(),
322            auto_response_enabled: true,
323        };
324        manager.load_default_playbooks();
325        manager
326    }
327
328    /// Load default response playbooks
329    fn load_default_playbooks(&mut self) {
330        // Brute Force Response Playbook
331        self.playbooks.push(Playbook {
332            id: "PB-001".to_string(),
333            name: "Brute Force Attack Response".to_string(),
334            description: "Automated response to brute force attacks".to_string(),
335            threat_categories: vec![ThreatCategory::BruteForce],
336            min_severity: ThreatSeverity::Medium,
337            actions: vec![
338                PlaybookAction {
339                    action: ResponseAction::BlockIP {
340                        ip: "{{source_ip}}".to_string(),
341                        duration_hours: 24,
342                    },
343                    order: 1,
344                    condition: None,
345                    timeout_seconds: 30,
346                    on_failure: FailureAction::Continue,
347                },
348                PlaybookAction {
349                    action: ResponseAction::NotifyTeam {
350                        team: "Security".to_string(),
351                        message: "Brute force attack detected".to_string(),
352                    },
353                    order: 2,
354                    condition: None,
355                    timeout_seconds: 10,
356                    on_failure: FailureAction::Continue,
357                },
358                PlaybookAction {
359                    action: ResponseAction::CollectEvidence {
360                        sources: vec!["auth_logs".to_string(), "firewall_logs".to_string()],
361                    },
362                    order: 3,
363                    condition: None,
364                    timeout_seconds: 60,
365                    on_failure: FailureAction::Continue,
366                },
367            ],
368            enabled: true,
369            requires_approval: false,
370        });
371
372        // Malware Response Playbook
373        self.playbooks.push(Playbook {
374            id: "PB-002".to_string(),
375            name: "Malware Detection Response".to_string(),
376            description: "Critical response to malware detection".to_string(),
377            threat_categories: vec![ThreatCategory::MalwareDetection],
378            min_severity: ThreatSeverity::High,
379            actions: vec![
380                PlaybookAction {
381                    action: ResponseAction::IsolateHost {
382                        hostname: "{{hostname}}".to_string(),
383                    },
384                    order: 1,
385                    condition: None,
386                    timeout_seconds: 60,
387                    on_failure: FailureAction::Escalate,
388                },
389                PlaybookAction {
390                    action: ResponseAction::EscalateToSOC {
391                        priority: "Critical".to_string(),
392                    },
393                    order: 2,
394                    condition: None,
395                    timeout_seconds: 10,
396                    on_failure: FailureAction::Continue,
397                },
398                PlaybookAction {
399                    action: ResponseAction::RunScan {
400                        scan_type: "full_antivirus".to_string(),
401                        target: "{{hostname}}".to_string(),
402                    },
403                    order: 3,
404                    condition: None,
405                    timeout_seconds: 300,
406                    on_failure: FailureAction::Continue,
407                },
408                PlaybookAction {
409                    action: ResponseAction::CollectEvidence {
410                        sources: vec!["memory_dump".to_string(), "process_list".to_string(), "network_connections".to_string()],
411                    },
412                    order: 4,
413                    condition: None,
414                    timeout_seconds: 120,
415                    on_failure: FailureAction::Continue,
416                },
417            ],
418            enabled: true,
419            requires_approval: false,
420        });
421
422        // Data Exfiltration Response Playbook
423        self.playbooks.push(Playbook {
424            id: "PB-003".to_string(),
425            name: "Data Exfiltration Response".to_string(),
426            description: "Response to potential data theft".to_string(),
427            threat_categories: vec![ThreatCategory::DataExfiltration],
428            min_severity: ThreatSeverity::High,
429            actions: vec![
430                PlaybookAction {
431                    action: ResponseAction::BlockIP {
432                        ip: "{{destination_ip}}".to_string(),
433                        duration_hours: 168, // 1 week
434                    },
435                    order: 1,
436                    condition: None,
437                    timeout_seconds: 30,
438                    on_failure: FailureAction::Continue,
439                },
440                PlaybookAction {
441                    action: ResponseAction::TerminateSession {
442                        session_id: "{{session_id}}".to_string(),
443                    },
444                    order: 2,
445                    condition: None,
446                    timeout_seconds: 10,
447                    on_failure: FailureAction::Continue,
448                },
449                PlaybookAction {
450                    action: ResponseAction::DisableAccount {
451                        account: "{{username}}".to_string(),
452                    },
453                    order: 3,
454                    condition: Some("severity >= High".to_string()),
455                    timeout_seconds: 10,
456                    on_failure: FailureAction::Escalate,
457                },
458                PlaybookAction {
459                    action: ResponseAction::CreateTicket {
460                        system: "ServiceNow".to_string(),
461                        priority: "P1".to_string(),
462                    },
463                    order: 4,
464                    condition: None,
465                    timeout_seconds: 30,
466                    on_failure: FailureAction::Continue,
467                },
468            ],
469            enabled: true,
470            requires_approval: true,
471        });
472
473        // Unauthorized Access Response Playbook
474        self.playbooks.push(Playbook {
475            id: "PB-004".to_string(),
476            name: "Unauthorized Access Response".to_string(),
477            description: "Response to privilege escalation and unauthorized access".to_string(),
478            threat_categories: vec![ThreatCategory::UnauthorizedAccess],
479            min_severity: ThreatSeverity::High,
480            actions: vec![
481                PlaybookAction {
482                    action: ResponseAction::DisableAccount {
483                        account: "{{username}}".to_string(),
484                    },
485                    order: 1,
486                    condition: None,
487                    timeout_seconds: 10,
488                    on_failure: FailureAction::Escalate,
489                },
490                PlaybookAction {
491                    action: ResponseAction::ResetCredentials {
492                        account: "{{username}}".to_string(),
493                    },
494                    order: 2,
495                    condition: None,
496                    timeout_seconds: 30,
497                    on_failure: FailureAction::Continue,
498                },
499                PlaybookAction {
500                    action: ResponseAction::EnableMFA {
501                        account: "{{username}}".to_string(),
502                    },
503                    order: 3,
504                    condition: None,
505                    timeout_seconds: 30,
506                    on_failure: FailureAction::Continue,
507                },
508                PlaybookAction {
509                    action: ResponseAction::EscalateToSOC {
510                        priority: "High".to_string(),
511                    },
512                    order: 4,
513                    condition: None,
514                    timeout_seconds: 10,
515                    on_failure: FailureAction::Continue,
516                },
517            ],
518            enabled: true,
519            requires_approval: false,
520        });
521    }
522
523    /// Create incident from alert
524    pub fn create_incident(&mut self, alert: &ThreatAlert) -> String {
525        let incident = Incident::from_alert(alert);
526        let id = incident.id.clone();
527        self.incidents.insert(id.clone(), incident);
528        id
529    }
530
531    /// Get incident by ID
532    pub fn get_incident(&self, id: &str) -> Option<&Incident> {
533        self.incidents.get(id)
534    }
535
536    /// Get mutable incident
537    pub fn get_incident_mut(&mut self, id: &str) -> Option<&mut Incident> {
538        self.incidents.get_mut(id)
539    }
540
541    /// Find applicable playbooks for an alert
542    pub fn find_playbooks(&self, alert: &ThreatAlert) -> Vec<&Playbook> {
543        self.playbooks
544            .iter()
545            .filter(|pb| {
546                pb.enabled
547                    && pb.threat_categories.contains(&alert.category)
548                    && alert.severity >= pb.min_severity
549            })
550            .collect()
551    }
552
553    /// Execute playbook actions (simulated)
554    pub fn execute_playbook(&mut self, incident_id: &str, playbook: &Playbook, context: &HashMap<String, String>) -> Vec<ActionResult> {
555        let mut results = Vec::new();
556
557        for pb_action in &playbook.actions {
558            let action = self.substitute_variables(&pb_action.action, context);
559
560            // Simulate action execution
561            let result = ActionResult {
562                action,
563                success: true, // In real implementation, this would be actual execution
564                executed_at: Utc::now(),
565                message: "Action executed successfully".to_string(),
566                execution_time_ms: 100,
567            };
568
569            results.push(result.clone());
570
571            // Update incident if it exists
572            if let Some(incident) = self.incidents.get_mut(incident_id) {
573                incident.add_action_result(result);
574            }
575        }
576
577        results
578    }
579
580    /// Substitute variables in action
581    fn substitute_variables(&self, action: &ResponseAction, context: &HashMap<String, String>) -> ResponseAction {
582        match action {
583            ResponseAction::BlockIP { ip, duration_hours } => {
584                ResponseAction::BlockIP {
585                    ip: self.substitute_var(ip, context),
586                    duration_hours: *duration_hours,
587                }
588            }
589            ResponseAction::DisableAccount { account } => {
590                ResponseAction::DisableAccount {
591                    account: self.substitute_var(account, context),
592                }
593            }
594            ResponseAction::IsolateHost { hostname } => {
595                ResponseAction::IsolateHost {
596                    hostname: self.substitute_var(hostname, context),
597                }
598            }
599            ResponseAction::TerminateSession { session_id } => {
600                ResponseAction::TerminateSession {
601                    session_id: self.substitute_var(session_id, context),
602                }
603            }
604            ResponseAction::RunScan { scan_type, target } => {
605                ResponseAction::RunScan {
606                    scan_type: scan_type.clone(),
607                    target: self.substitute_var(target, context),
608                }
609            }
610            ResponseAction::ResetCredentials { account } => {
611                ResponseAction::ResetCredentials {
612                    account: self.substitute_var(account, context),
613                }
614            }
615            ResponseAction::EnableMFA { account } => {
616                ResponseAction::EnableMFA {
617                    account: self.substitute_var(account, context),
618                }
619            }
620            _ => action.clone(),
621        }
622    }
623
624    /// Substitute variable placeholders
625    fn substitute_var(&self, template: &str, context: &HashMap<String, String>) -> String {
626        let mut result = template.to_string();
627        for (key, value) in context {
628            result = result.replace(&format!("{{{{{}}}}}", key), value);
629        }
630        result
631    }
632
633    /// Get active incidents
634    pub fn get_active_incidents(&self) -> Vec<&Incident> {
635        self.incidents
636            .values()
637            .filter(|i| i.status.is_active())
638            .collect()
639    }
640
641    /// Get incidents by severity
642    pub fn get_incidents_by_severity(&self, min_severity: ThreatSeverity) -> Vec<&Incident> {
643        self.incidents
644            .values()
645            .filter(|i| i.severity >= min_severity)
646            .collect()
647    }
648
649    /// Get overdue incidents
650    pub fn get_overdue_incidents(&self, sla_hours: i64) -> Vec<&Incident> {
651        self.incidents
652            .values()
653            .filter(|i| i.is_overdue(sla_hours))
654            .collect()
655    }
656
657    /// Get incident statistics
658    pub fn get_statistics(&self) -> IncidentStatistics {
659        let total = self.incidents.len();
660        let active = self.incidents.values().filter(|i| i.status.is_active()).count();
661        let resolved = self.incidents.values().filter(|i| matches!(i.status, IncidentStatus::Resolved | IncidentStatus::Closed)).count();
662
663        let mut by_severity: HashMap<ThreatSeverity, usize> = HashMap::new();
664        let mut by_category: HashMap<ThreatCategory, usize> = HashMap::new();
665
666        for incident in self.incidents.values() {
667            *by_severity.entry(incident.severity).or_insert(0) += 1;
668            *by_category.entry(incident.category.clone()).or_insert(0) += 1;
669        }
670
671        let avg_resolution_time = self.calculate_avg_resolution_time();
672
673        IncidentStatistics {
674            total_incidents: total,
675            active_incidents: active,
676            resolved_incidents: resolved,
677            by_severity,
678            by_category,
679            average_resolution_time_hours: avg_resolution_time,
680        }
681    }
682
683    /// Calculate average resolution time
684    fn calculate_avg_resolution_time(&self) -> f64 {
685        let resolved: Vec<&Incident> = self.incidents
686            .values()
687            .filter(|i| i.metrics.time_to_resolve_seconds.is_some())
688            .collect();
689
690        if resolved.is_empty() {
691            return 0.0;
692        }
693
694        let total_seconds: i64 = resolved
695            .iter()
696            .filter_map(|i| i.metrics.time_to_resolve_seconds)
697            .sum();
698
699        (total_seconds as f64 / resolved.len() as f64) / 3600.0
700    }
701
702    /// Enable/disable auto response
703    pub fn set_auto_response(&mut self, enabled: bool) {
704        self.auto_response_enabled = enabled;
705    }
706
707    /// Add custom playbook
708    pub fn add_playbook(&mut self, playbook: Playbook) {
709        self.playbooks.push(playbook);
710    }
711
712    /// Get all playbooks
713    pub fn get_playbooks(&self) -> &[Playbook] {
714        &self.playbooks
715    }
716}
717
718impl Default for IncidentResponseManager {
719    fn default() -> Self {
720        Self::new()
721    }
722}
723
724/// Incident statistics
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct IncidentStatistics {
727    pub total_incidents: usize,
728    pub active_incidents: usize,
729    pub resolved_incidents: usize,
730    pub by_severity: HashMap<ThreatSeverity, usize>,
731    pub by_category: HashMap<ThreatCategory, usize>,
732    pub average_resolution_time_hours: f64,
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    fn create_test_alert() -> ThreatAlert {
740        ThreatAlert {
741            alert_id: "TEST-001".to_string(),
742            timestamp: Utc::now(),
743            severity: ThreatSeverity::High,
744            category: ThreatCategory::BruteForce,
745            description: "Test brute force alert".to_string(),
746            source_log: "Test log".to_string(),
747            indicators: vec!["192.168.1.100".to_string()],
748            recommended_action: "Block IP".to_string(),
749            threat_score: 80,
750            correlated_alerts: vec![],
751        }
752    }
753
754    #[test]
755    fn test_incident_creation() {
756        let alert = create_test_alert();
757        let incident = Incident::from_alert(&alert);
758
759        assert_eq!(incident.status, IncidentStatus::New);
760        assert_eq!(incident.severity, ThreatSeverity::High);
761        assert_eq!(incident.related_alerts.len(), 1);
762    }
763
764    #[test]
765    fn test_incident_status_progression() {
766        let alert = create_test_alert();
767        let mut incident = Incident::from_alert(&alert);
768
769        incident.update_status(IncidentStatus::Acknowledged, "analyst");
770        assert!(incident.acknowledged_at.is_some());
771        assert!(incident.metrics.time_to_acknowledge_seconds.is_some());
772
773        incident.update_status(IncidentStatus::Resolved, "analyst");
774        assert!(incident.resolved_at.is_some());
775        assert!(incident.metrics.time_to_resolve_seconds.is_some());
776    }
777
778    #[test]
779    fn test_incident_notes() {
780        let alert = create_test_alert();
781        let mut incident = Incident::from_alert(&alert);
782
783        incident.add_note("analyst", "Initial investigation started");
784        assert_eq!(incident.notes.len(), 1);
785        assert_eq!(incident.notes[0].author, "analyst");
786    }
787
788    #[test]
789    fn test_playbook_finding() {
790        let manager = IncidentResponseManager::new();
791        let alert = create_test_alert();
792
793        let playbooks = manager.find_playbooks(&alert);
794        assert!(!playbooks.is_empty());
795
796        // Should find brute force playbook
797        assert!(playbooks.iter().any(|pb| pb.name.contains("Brute Force")));
798    }
799
800    #[test]
801    fn test_playbook_execution() {
802        let mut manager = IncidentResponseManager::new();
803        let alert = create_test_alert();
804
805        let incident_id = manager.create_incident(&alert);
806
807        // Clone the playbook to avoid borrow checker issues
808        let playbook: Option<Playbook> = {
809            manager.find_playbooks(&alert).first().map(|&p| p.clone())
810        };
811
812        let mut context = HashMap::new();
813        context.insert("source_ip".to_string(), "192.168.1.100".to_string());
814
815        if let Some(playbook) = playbook {
816            let results = manager.execute_playbook(&incident_id, &playbook, &context);
817            assert!(!results.is_empty());
818            assert!(results[0].success);
819        }
820    }
821
822    #[test]
823    fn test_variable_substitution() {
824        let manager = IncidentResponseManager::new();
825        let mut context = HashMap::new();
826        context.insert("source_ip".to_string(), "10.0.0.1".to_string());
827
828        let action = ResponseAction::BlockIP {
829            ip: "{{source_ip}}".to_string(),
830            duration_hours: 24,
831        };
832
833        let substituted = manager.substitute_variables(&action, &context);
834        if let ResponseAction::BlockIP { ip, .. } = substituted {
835            assert_eq!(ip, "10.0.0.1");
836        }
837    }
838
839    #[test]
840    fn test_active_incidents() {
841        let mut manager = IncidentResponseManager::new();
842
843        let alert1 = create_test_alert();
844        let id1 = manager.create_incident(&alert1);
845
846        let alert2 = create_test_alert();
847        let id2 = manager.create_incident(&alert2);
848
849        // Resolve one incident
850        if let Some(incident) = manager.get_incident_mut(&id1) {
851            incident.update_status(IncidentStatus::Resolved, "analyst");
852        }
853
854        let active = manager.get_active_incidents();
855        assert_eq!(active.len(), 1);
856    }
857
858    #[test]
859    fn test_incident_statistics() {
860        let mut manager = IncidentResponseManager::new();
861
862        for _ in 0..5 {
863            let alert = create_test_alert();
864            manager.create_incident(&alert);
865        }
866
867        let stats = manager.get_statistics();
868        assert_eq!(stats.total_incidents, 5);
869        assert_eq!(stats.active_incidents, 5);
870    }
871
872    #[test]
873    fn test_incident_overdue() {
874        let alert = create_test_alert();
875        let mut incident = Incident::from_alert(&alert);
876
877        // New incident should not be overdue with reasonable SLA
878        assert!(!incident.is_overdue(24));
879
880        // Resolved incident should not be overdue
881        incident.update_status(IncidentStatus::Resolved, "analyst");
882        assert!(!incident.is_overdue(1));
883    }
884
885    #[test]
886    fn test_action_reversibility() {
887        let block_action = ResponseAction::BlockIP {
888            ip: "10.0.0.1".to_string(),
889            duration_hours: 24,
890        };
891        assert!(block_action.is_reversible());
892
893        let notify_action = ResponseAction::NotifyTeam {
894            team: "Security".to_string(),
895            message: "Test".to_string(),
896        };
897        assert!(!notify_action.is_reversible());
898    }
899
900    #[test]
901    fn test_status_next() {
902        assert_eq!(IncidentStatus::New.next(), Some(IncidentStatus::Acknowledged));
903        assert_eq!(IncidentStatus::Resolved.next(), Some(IncidentStatus::Closed));
904        assert_eq!(IncidentStatus::Closed.next(), None);
905    }
906}