1use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11use crate::{ThreatAlert, ThreatCategory, ThreatSeverity};
12
13#[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 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 pub fn is_active(&self) -> bool {
43 !matches!(self, IncidentStatus::Resolved | IncidentStatus::Closed)
44 }
45}
46
47#[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 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 pub fn is_reversible(&self) -> bool {
87 matches!(
88 self,
89 ResponseAction::BlockIP { .. }
90 | ResponseAction::DisableAccount { .. }
91 | ResponseAction::IsolateHost { .. }
92 )
93 }
94}
95
96#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
131pub enum FailureAction {
132 Continue,
133 Stop,
134 Retry { max_attempts: u32 },
135 Escalate,
136}
137
138#[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#[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#[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#[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 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 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 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 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 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 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 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
309pub struct IncidentResponseManager {
311 incidents: HashMap<String, Incident>,
312 playbooks: Vec<Playbook>,
313 auto_response_enabled: bool,
314}
315
316impl IncidentResponseManager {
317 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 fn load_default_playbooks(&mut self) {
330 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 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 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, },
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 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 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 pub fn get_incident(&self, id: &str) -> Option<&Incident> {
533 self.incidents.get(id)
534 }
535
536 pub fn get_incident_mut(&mut self, id: &str) -> Option<&mut Incident> {
538 self.incidents.get_mut(id)
539 }
540
541 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 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 let result = ActionResult {
562 action,
563 success: true, 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 if let Some(incident) = self.incidents.get_mut(incident_id) {
573 incident.add_action_result(result);
574 }
575 }
576
577 results
578 }
579
580 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 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 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 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 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 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 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 pub fn set_auto_response(&mut self, enabled: bool) {
704 self.auto_response_enabled = enabled;
705 }
706
707 pub fn add_playbook(&mut self, playbook: Playbook) {
709 self.playbooks.push(playbook);
710 }
711
712 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#[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 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 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 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 assert!(!incident.is_overdue(24));
879
880 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}