1use parking_lot::RwLock;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, VecDeque};
19use std::fs::{File, OpenOptions};
20use std::io::{BufWriter, Write};
21use std::path::PathBuf;
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
24
25pub const DEFAULT_FAILED_LOGIN_THRESHOLD: u32 = 5;
31
32pub const DEFAULT_FAILED_LOGIN_WINDOW_SECS: u64 = 300;
34
35pub const DEFAULT_UNUSUAL_ACCESS_THRESHOLD: u32 = 100;
37
38pub const DEFAULT_UNUSUAL_ACCESS_WINDOW_SECS: u64 = 60;
40
41pub const DEFAULT_MASS_DATA_THRESHOLD: u64 = 1000;
43
44pub const MAX_EVENTS_IN_MEMORY: usize = 10000;
46
47pub const MAX_INCIDENTS_IN_MEMORY: usize = 1000;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum SecurityEventType {
58 FailedLogin,
60 UnauthorizedAccess,
62 UnusualAccessPattern,
64 AdminFromUnknownIp,
66 MassDataExport,
68 MassDataDeletion,
70 SessionHijacking,
72 SqlInjection,
74 BruteForceAttack,
76 PrivilegeEscalation,
78}
79
80impl std::fmt::Display for SecurityEventType {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 SecurityEventType::FailedLogin => write!(f, "failed_login"),
84 SecurityEventType::UnauthorizedAccess => write!(f, "unauthorized_access"),
85 SecurityEventType::UnusualAccessPattern => write!(f, "unusual_access_pattern"),
86 SecurityEventType::AdminFromUnknownIp => write!(f, "admin_from_unknown_ip"),
87 SecurityEventType::MassDataExport => write!(f, "mass_data_export"),
88 SecurityEventType::MassDataDeletion => write!(f, "mass_data_deletion"),
89 SecurityEventType::SessionHijacking => write!(f, "session_hijacking"),
90 SecurityEventType::SqlInjection => write!(f, "sql_injection"),
91 SecurityEventType::BruteForceAttack => write!(f, "brute_force_attack"),
92 SecurityEventType::PrivilegeEscalation => write!(f, "privilege_escalation"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct SecurityEvent {
100 pub id: String,
102 pub event_type: SecurityEventType,
104 pub timestamp: u64,
106 pub user: Option<String>,
108 pub ip_address: Option<String>,
110 pub resource: Option<String>,
112 pub description: String,
114 pub metadata: HashMap<String, String>,
116}
117
118impl SecurityEvent {
119 pub fn new(event_type: SecurityEventType, description: &str, counter: &AtomicU64) -> Self {
121 Self {
122 id: generate_event_id(counter),
123 event_type,
124 timestamp: now_timestamp(),
125 user: None,
126 ip_address: None,
127 resource: None,
128 description: description.to_string(),
129 metadata: HashMap::new(),
130 }
131 }
132
133 pub fn with_user(mut self, user: &str) -> Self {
135 self.user = Some(user.to_string());
136 self
137 }
138
139 pub fn with_ip(mut self, ip: &str) -> Self {
141 self.ip_address = Some(ip.to_string());
142 self
143 }
144
145 pub fn with_resource(mut self, resource: &str) -> Self {
147 self.resource = Some(resource.to_string());
148 self
149 }
150
151 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
153 self.metadata.insert(key.to_string(), value.to_string());
154 self
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
164#[serde(rename_all = "lowercase")]
165pub enum BreachSeverity {
166 Low,
168 Medium,
170 High,
172 Critical,
174}
175
176impl std::fmt::Display for BreachSeverity {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 BreachSeverity::Low => write!(f, "low"),
180 BreachSeverity::Medium => write!(f, "medium"),
181 BreachSeverity::High => write!(f, "high"),
182 BreachSeverity::Critical => write!(f, "critical"),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum IncidentStatus {
195 Open,
197 Acknowledged,
199 Investigating,
201 Resolved,
203 FalsePositive,
205}
206
207impl std::fmt::Display for IncidentStatus {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 match self {
210 IncidentStatus::Open => write!(f, "open"),
211 IncidentStatus::Acknowledged => write!(f, "acknowledged"),
212 IncidentStatus::Investigating => write!(f, "investigating"),
213 IncidentStatus::Resolved => write!(f, "resolved"),
214 IncidentStatus::FalsePositive => write!(f, "false_positive"),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct BreachIncident {
222 pub id: String,
224 pub detected_at: u64,
226 pub incident_type: SecurityEventType,
228 pub severity: BreachSeverity,
230 pub affected_subjects: Vec<String>,
232 pub status: IncidentStatus,
234 pub notified: bool,
236 pub notification_timestamps: HashMap<String, u64>,
238 pub description: String,
240 pub related_events: Vec<String>,
242 pub involved_ips: Vec<String>,
244 pub details: HashMap<String, String>,
246 pub acknowledged_by: Option<String>,
248 pub acknowledged_at: Option<u64>,
250 pub resolution_notes: Option<String>,
252 pub resolved_at: Option<u64>,
254}
255
256impl BreachIncident {
257 pub fn new(
259 incident_type: SecurityEventType,
260 severity: BreachSeverity,
261 description: &str,
262 counter: &AtomicU64,
263 ) -> Self {
264 Self {
265 id: generate_incident_id(counter),
266 detected_at: now_timestamp(),
267 incident_type,
268 severity,
269 affected_subjects: Vec::new(),
270 status: IncidentStatus::Open,
271 notified: false,
272 notification_timestamps: HashMap::new(),
273 description: description.to_string(),
274 related_events: Vec::new(),
275 involved_ips: Vec::new(),
276 details: HashMap::new(),
277 acknowledged_by: None,
278 acknowledged_at: None,
279 resolution_notes: None,
280 resolved_at: None,
281 }
282 }
283
284 pub fn with_affected_subject(mut self, subject: &str) -> Self {
286 if !self.affected_subjects.contains(&subject.to_string()) {
287 self.affected_subjects.push(subject.to_string());
288 }
289 self
290 }
291
292 pub fn with_related_event(mut self, event_id: &str) -> Self {
294 self.related_events.push(event_id.to_string());
295 self
296 }
297
298 pub fn with_involved_ip(mut self, ip: &str) -> Self {
300 if !self.involved_ips.contains(&ip.to_string()) {
301 self.involved_ips.push(ip.to_string());
302 }
303 self
304 }
305
306 pub fn with_detail(mut self, key: &str, value: &str) -> Self {
308 self.details.insert(key.to_string(), value.to_string());
309 self
310 }
311
312 pub fn requires_immediate_notification(&self) -> bool {
314 self.severity >= BreachSeverity::High
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct DetectionConfig {
325 pub failed_login_threshold: u32,
327 pub failed_login_window_secs: u64,
329 pub unusual_access_threshold: u32,
331 pub unusual_access_window_secs: u64,
333 pub mass_data_threshold: u64,
335 pub trusted_admin_ips: Vec<String>,
337 pub enable_brute_force_detection: bool,
339 pub enable_sql_injection_detection: bool,
341}
342
343impl Default for DetectionConfig {
344 fn default() -> Self {
345 Self {
346 failed_login_threshold: DEFAULT_FAILED_LOGIN_THRESHOLD,
347 failed_login_window_secs: DEFAULT_FAILED_LOGIN_WINDOW_SECS,
348 unusual_access_threshold: DEFAULT_UNUSUAL_ACCESS_THRESHOLD,
349 unusual_access_window_secs: DEFAULT_UNUSUAL_ACCESS_WINDOW_SECS,
350 mass_data_threshold: DEFAULT_MASS_DATA_THRESHOLD,
351 trusted_admin_ips: vec!["127.0.0.1".to_string(), "::1".to_string()],
352 enable_brute_force_detection: true,
353 enable_sql_injection_detection: true,
354 }
355 }
356}
357
358pub trait BreachNotifier: Send + Sync {
364 fn name(&self) -> &str;
366
367 fn notify(&self, incident: &BreachIncident) -> Result<(), String>;
369}
370
371pub struct WebhookNotifier {
377 name: String,
378 url: String,
379 headers: HashMap<String, String>,
380 client: reqwest::blocking::Client,
381}
382
383impl WebhookNotifier {
384 pub fn new(url: &str) -> Self {
386 Self {
387 name: "webhook".to_string(),
388 url: url.to_string(),
389 headers: HashMap::new(),
390 client: reqwest::blocking::Client::builder()
391 .timeout(Duration::from_secs(30))
392 .build()
393 .unwrap_or_else(|_| reqwest::blocking::Client::new()),
394 }
395 }
396
397 pub fn with_name(mut self, name: &str) -> Self {
399 self.name = name.to_string();
400 self
401 }
402
403 pub fn with_header(mut self, key: &str, value: &str) -> Self {
405 self.headers.insert(key.to_string(), value.to_string());
406 self
407 }
408}
409
410impl BreachNotifier for WebhookNotifier {
411 fn name(&self) -> &str {
412 &self.name
413 }
414
415 fn notify(&self, incident: &BreachIncident) -> Result<(), String> {
416 let payload = serde_json::json!({
418 "incident_id": incident.id,
419 "detected_at": format_timestamp(incident.detected_at),
420 "type": incident.incident_type.to_string(),
421 "severity": incident.severity.to_string(),
422 "description": incident.description,
423 "affected_subjects": incident.affected_subjects,
424 "involved_ips": incident.involved_ips,
425 "status": incident.status.to_string(),
426 "details": incident.details,
427 "source": "aegis-db",
428 });
429
430 let mut request = self.client.post(&self.url);
431
432 for (key, value) in &self.headers {
434 request = request.header(key, value);
435 }
436
437 let response = request
438 .header("Content-Type", "application/json")
439 .json(&payload)
440 .send()
441 .map_err(|e| format!("Failed to send webhook: {}", e))?;
442
443 if response.status().is_success() {
444 Ok(())
445 } else {
446 Err(format!(
447 "Webhook returned error status: {}",
448 response.status()
449 ))
450 }
451 }
452}
453
454pub struct LogNotifier {
460 name: String,
461 log_path: PathBuf,
462 writer: RwLock<Option<BufWriter<File>>>,
463}
464
465impl LogNotifier {
466 pub fn new(log_path: PathBuf) -> std::io::Result<Self> {
468 if let Some(parent) = log_path.parent() {
470 std::fs::create_dir_all(parent)?;
471 }
472
473 let file = OpenOptions::new()
474 .create(true)
475 .append(true)
476 .open(&log_path)?;
477
478 Ok(Self {
479 name: "log".to_string(),
480 log_path,
481 writer: RwLock::new(Some(BufWriter::new(file))),
482 })
483 }
484
485 pub fn with_name(mut self, name: &str) -> Self {
487 self.name = name.to_string();
488 self
489 }
490}
491
492impl BreachNotifier for LogNotifier {
493 fn name(&self) -> &str {
494 &self.name
495 }
496
497 fn notify(&self, incident: &BreachIncident) -> Result<(), String> {
498 let log_entry = serde_json::json!({
499 "timestamp": format_timestamp(now_timestamp()),
500 "incident_id": incident.id,
501 "detected_at": format_timestamp(incident.detected_at),
502 "type": incident.incident_type.to_string(),
503 "severity": incident.severity.to_string(),
504 "description": incident.description,
505 "affected_subjects": incident.affected_subjects,
506 "involved_ips": incident.involved_ips,
507 "status": incident.status.to_string(),
508 "details": incident.details,
509 });
510
511 let mut writer = self.writer.write();
512 if writer.is_none() {
514 match OpenOptions::new().create(true).append(true).open(&self.log_path) {
515 Ok(file) => {
516 *writer = Some(BufWriter::new(file));
517 }
518 Err(e) => {
519 return Err(format!(
520 "Failed to reopen breach log {}: {}",
521 self.log_path.display(),
522 e
523 ));
524 }
525 }
526 }
527 if let Some(ref mut w) = *writer {
528 writeln!(w, "{}", log_entry)
529 .map_err(|e| format!("Failed to write to breach log: {}", e))?;
530 w.flush()
531 .map_err(|e| format!("Failed to flush breach log: {}", e))?;
532 Ok(())
533 } else {
534 Err("Log writer not initialized".to_string())
535 }
536 }
537}
538
539struct EventRecord {
545 event: SecurityEvent,
546 received_at: Instant,
547}
548
549pub struct BreachDetector {
551 config: RwLock<DetectionConfig>,
553 events: RwLock<HashMap<SecurityEventType, VecDeque<EventRecord>>>,
555 failed_logins: RwLock<HashMap<String, VecDeque<Instant>>>,
557 access_patterns: RwLock<HashMap<String, VecDeque<Instant>>>,
559 incidents: RwLock<VecDeque<BreachIncident>>,
561 incident_counter: AtomicU64,
563 event_counter: AtomicU64,
565 notifiers: RwLock<Vec<Box<dyn BreachNotifier>>>,
567 data_dir: Option<PathBuf>,
569}
570
571impl BreachDetector {
572 pub fn new() -> Self {
574 Self::with_data_dir(None)
575 }
576
577 pub fn with_data_dir(data_dir: Option<PathBuf>) -> Self {
582 let mut incidents = VecDeque::with_capacity(MAX_INCIDENTS_IN_MEMORY);
583 let mut counter: u64 = 1;
584
585 if let Some(ref dir) = data_dir {
586 let path = dir.join("breach_incidents.json");
587 if path.exists() {
588 match std::fs::read_to_string(&path) {
589 Ok(contents) => match serde_json::from_str::<Vec<BreachIncident>>(&contents) {
590 Ok(loaded) => {
591 let count = loaded.len();
592 for inc in loaded {
593 incidents.push_back(inc);
594 }
595 counter = (count as u64).saturating_add(1);
596 tracing::info!("Loaded {} breach incidents from disk", count);
597 }
598 Err(e) => {
599 tracing::error!(
600 "Failed to parse breach incidents from {}: {}",
601 path.display(),
602 e
603 );
604 }
605 },
606 Err(e) => {
607 tracing::error!(
608 "Failed to read breach incidents from {}: {}",
609 path.display(),
610 e
611 );
612 }
613 }
614 }
615 }
616
617 Self {
618 config: RwLock::new(DetectionConfig::default()),
619 events: RwLock::new(HashMap::new()),
620 failed_logins: RwLock::new(HashMap::new()),
621 access_patterns: RwLock::new(HashMap::new()),
622 incidents: RwLock::new(incidents),
623 incident_counter: AtomicU64::new(counter),
624 event_counter: AtomicU64::new(1),
625 notifiers: RwLock::new(Vec::new()),
626 data_dir,
627 }
628 }
629
630 pub fn with_config(config: DetectionConfig) -> Self {
632 Self {
633 config: RwLock::new(config),
634 events: RwLock::new(HashMap::new()),
635 failed_logins: RwLock::new(HashMap::new()),
636 access_patterns: RwLock::new(HashMap::new()),
637 incidents: RwLock::new(VecDeque::with_capacity(MAX_INCIDENTS_IN_MEMORY)),
638 incident_counter: AtomicU64::new(1),
639 event_counter: AtomicU64::new(1),
640 notifiers: RwLock::new(Vec::new()),
641 data_dir: None,
642 }
643 }
644
645 fn flush_incidents_to_disk(&self) {
647 let Some(ref dir) = self.data_dir else {
648 return;
649 };
650 let path = dir.join("breach_incidents.json");
651 let incidents = self.incidents.read();
652 let vec: Vec<&BreachIncident> = incidents.iter().collect();
653 match serde_json::to_string_pretty(&vec) {
654 Ok(json) => {
655 if let Err(e) = std::fs::write(&path, json) {
656 tracing::error!(
657 "Failed to write breach incidents to {}: {}",
658 path.display(),
659 e
660 );
661 }
662 }
663 Err(e) => {
664 tracing::error!("Failed to serialize breach incidents: {}", e);
665 }
666 }
667 }
668
669 pub fn update_config(&self, config: DetectionConfig) {
671 *self.config.write() = config;
672 }
673
674 pub fn get_config(&self) -> DetectionConfig {
676 self.config.read().clone()
677 }
678
679 pub fn register_notifier(&self, notifier: Box<dyn BreachNotifier>) {
681 self.notifiers.write().push(notifier);
682 }
683
684 pub fn record_event(&self, event: SecurityEvent) -> Option<BreachIncident> {
686 let event_type = event.event_type;
687 let now = Instant::now();
688
689 {
691 let mut events = self.events.write();
692 let queue = events
693 .entry(event_type)
694 .or_insert_with(|| VecDeque::with_capacity(MAX_EVENTS_IN_MEMORY));
695
696 while queue.len() >= MAX_EVENTS_IN_MEMORY {
698 queue.pop_front();
699 }
700
701 queue.push_back(EventRecord {
702 event: event.clone(),
703 received_at: now,
704 });
705 }
706
707 match event_type {
709 SecurityEventType::FailedLogin => self.check_failed_login_pattern(&event),
710 SecurityEventType::UnauthorizedAccess => self.check_unauthorized_access(&event),
711 SecurityEventType::UnusualAccessPattern => self.check_unusual_access_pattern(&event),
712 SecurityEventType::AdminFromUnknownIp => self.check_admin_unknown_ip(&event),
713 SecurityEventType::MassDataExport => self.check_mass_data_operation(&event, false),
714 SecurityEventType::MassDataDeletion => self.check_mass_data_operation(&event, true),
715 SecurityEventType::SessionHijacking => self.create_high_severity_incident(&event),
716 SecurityEventType::SqlInjection => self.check_sql_injection(&event),
717 SecurityEventType::BruteForceAttack => self.create_critical_incident(&event),
718 SecurityEventType::PrivilegeEscalation => self.create_critical_incident(&event),
719 }
720 }
721
722 pub fn record_failed_login(&self, username: &str, ip: Option<&str>) -> Option<BreachIncident> {
724 let event = SecurityEvent::new(
725 SecurityEventType::FailedLogin,
726 &format!("Failed login attempt for user: {}", username),
727 &self.event_counter,
728 )
729 .with_user(username);
730
731 let event = if let Some(ip) = ip {
732 event.with_ip(ip)
733 } else {
734 event
735 };
736
737 self.record_event(event)
738 }
739
740 pub fn check_failed_login(&self, ip: &str, username: &str) -> Option<BreachIncident> {
744 self.record_failed_login(username, Some(ip))
745 }
746
747 pub fn check_mass_access(&self, user: &str, count: u64) -> Option<BreachIncident> {
750 let config = self.config.read();
751 let threshold = config.mass_data_threshold;
752 drop(config);
753
754 if count >= threshold {
755 let event = SecurityEvent::new(
756 SecurityEventType::MassDataExport,
757 &format!(
758 "Mass data access detected: {} accessed {} records",
759 user, count
760 ),
761 &self.event_counter,
762 )
763 .with_user(user)
764 .with_metadata("record_count", &count.to_string());
765
766 return self.record_event(event);
767 }
768
769 None
770 }
771
772 pub fn record_unauthorized_access(
774 &self,
775 user: &str,
776 resource: &str,
777 permission: &str,
778 ip: Option<&str>,
779 ) -> Option<BreachIncident> {
780 let event = SecurityEvent::new(
781 SecurityEventType::UnauthorizedAccess,
782 &format!(
783 "Unauthorized access attempt: {} tried to {} on {}",
784 user, permission, resource
785 ),
786 &self.event_counter,
787 )
788 .with_user(user)
789 .with_resource(resource)
790 .with_metadata("permission", permission);
791
792 let event = if let Some(ip) = ip {
793 event.with_ip(ip)
794 } else {
795 event
796 };
797
798 self.record_event(event)
799 }
800
801 pub fn record_data_access(&self, user: &str, resource: &str, row_count: u64) {
803 let now = Instant::now();
804
805 {
807 let mut patterns = self.access_patterns.write();
808 let queue = patterns
809 .entry(user.to_string())
810 .or_insert_with(VecDeque::new);
811 queue.push_back(now);
812 }
813
814 let config = self.config.read();
816 let window = Duration::from_secs(config.unusual_access_window_secs);
817 let threshold = config.unusual_access_threshold;
818 drop(config);
819
820 let access_count = {
821 let mut patterns = self.access_patterns.write();
822 if let Some(queue) = patterns.get_mut(user) {
823 while let Some(front) = queue.front() {
825 if now.duration_since(*front) > window {
826 queue.pop_front();
827 } else {
828 break;
829 }
830 }
831 queue.len() as u32
832 } else {
833 0
834 }
835 };
836
837 if access_count >= threshold {
838 let event = SecurityEvent::new(
839 SecurityEventType::UnusualAccessPattern,
840 &format!(
841 "High volume data access detected: {} accessed {} rows from {}",
842 user, row_count, resource
843 ),
844 &self.event_counter,
845 )
846 .with_user(user)
847 .with_resource(resource)
848 .with_metadata("access_count", &access_count.to_string())
849 .with_metadata("row_count", &row_count.to_string());
850
851 self.record_event(event);
852 }
853 }
854
855 pub fn record_admin_action(
857 &self,
858 user: &str,
859 action: &str,
860 ip: &str,
861 ) -> Option<BreachIncident> {
862 let config = self.config.read();
863 let trusted_ips = config.trusted_admin_ips.clone();
864 drop(config);
865
866 if !trusted_ips.contains(&ip.to_string()) {
867 let event = SecurityEvent::new(
868 SecurityEventType::AdminFromUnknownIp,
869 &format!(
870 "Admin action '{}' performed by {} from untrusted IP {}",
871 action, user, ip
872 ),
873 &self.event_counter,
874 )
875 .with_user(user)
876 .with_ip(ip)
877 .with_metadata("action", action);
878
879 self.record_event(event)
880 } else {
881 None
882 }
883 }
884
885 pub fn record_mass_data_operation(
887 &self,
888 user: &str,
889 resource: &str,
890 row_count: u64,
891 is_deletion: bool,
892 ) -> Option<BreachIncident> {
893 let config = self.config.read();
894 let threshold = config.mass_data_threshold;
895 drop(config);
896
897 if row_count >= threshold {
898 let event_type = if is_deletion {
899 SecurityEventType::MassDataDeletion
900 } else {
901 SecurityEventType::MassDataExport
902 };
903
904 let operation = if is_deletion { "deleted" } else { "exported" };
905 let event = SecurityEvent::new(
906 event_type,
907 &format!(
908 "Mass data operation: {} {} {} rows from {}",
909 user, operation, row_count, resource
910 ),
911 &self.event_counter,
912 )
913 .with_user(user)
914 .with_resource(resource)
915 .with_metadata("row_count", &row_count.to_string());
916
917 self.record_event(event)
918 } else {
919 None
920 }
921 }
922
923 fn check_failed_login_pattern(&self, event: &SecurityEvent) -> Option<BreachIncident> {
925 let key = event
926 .user
927 .clone()
928 .or_else(|| event.ip_address.clone())
929 .unwrap_or_else(|| "unknown".to_string());
930
931 let now = Instant::now();
932 let config = self.config.read();
933 let window = Duration::from_secs(config.failed_login_window_secs);
934 let threshold = config.failed_login_threshold;
935 drop(config);
936
937 let count = {
939 let mut logins = self.failed_logins.write();
940 let queue = logins.entry(key.clone()).or_insert_with(VecDeque::new);
941 queue.push_back(now);
942
943 while let Some(front) = queue.front() {
945 if now.duration_since(*front) > window {
946 queue.pop_front();
947 } else {
948 break;
949 }
950 }
951 queue.len() as u32
952 };
953
954 if count >= threshold {
955 {
957 let mut logins = self.failed_logins.write();
958 logins.remove(&key);
959 }
960
961 let severity = if count >= threshold * 2 {
962 BreachSeverity::High
963 } else {
964 BreachSeverity::Medium
965 };
966
967 let mut incident = BreachIncident::new(
968 SecurityEventType::FailedLogin,
969 severity,
970 &format!(
971 "Multiple failed login attempts detected: {} attempts in {} seconds",
972 count,
973 window.as_secs()
974 ),
975 &self.incident_counter,
976 )
977 .with_related_event(&event.id)
978 .with_detail("attempt_count", &count.to_string());
979
980 if let Some(ref user) = event.user {
981 incident = incident.with_affected_subject(user);
982 }
983 if let Some(ref ip) = event.ip_address {
984 incident = incident.with_involved_ip(ip);
985 }
986
987 return self.create_and_notify_incident(incident);
988 }
989
990 None
991 }
992
993 fn check_unauthorized_access(&self, event: &SecurityEvent) -> Option<BreachIncident> {
995 let severity = BreachSeverity::Medium;
996
997 let mut incident = BreachIncident::new(
998 SecurityEventType::UnauthorizedAccess,
999 severity,
1000 &event.description,
1001 &self.incident_counter,
1002 )
1003 .with_related_event(&event.id);
1004
1005 if let Some(ref user) = event.user {
1006 incident = incident.with_affected_subject(user);
1007 }
1008 if let Some(ref ip) = event.ip_address {
1009 incident = incident.with_involved_ip(ip);
1010 }
1011 if let Some(ref resource) = event.resource {
1012 incident = incident.with_detail("resource", resource);
1013 }
1014
1015 self.create_and_notify_incident(incident)
1016 }
1017
1018 fn check_unusual_access_pattern(&self, event: &SecurityEvent) -> Option<BreachIncident> {
1020 let mut incident = BreachIncident::new(
1021 SecurityEventType::UnusualAccessPattern,
1022 BreachSeverity::Medium,
1023 &event.description,
1024 &self.incident_counter,
1025 )
1026 .with_related_event(&event.id);
1027
1028 if let Some(ref user) = event.user {
1029 incident = incident.with_affected_subject(user);
1030 }
1031 if let Some(ref ip) = event.ip_address {
1032 incident = incident.with_involved_ip(ip);
1033 }
1034
1035 self.create_and_notify_incident(incident)
1036 }
1037
1038 fn check_admin_unknown_ip(&self, event: &SecurityEvent) -> Option<BreachIncident> {
1040 let mut incident = BreachIncident::new(
1041 SecurityEventType::AdminFromUnknownIp,
1042 BreachSeverity::High,
1043 &event.description,
1044 &self.incident_counter,
1045 )
1046 .with_related_event(&event.id);
1047
1048 if let Some(ref user) = event.user {
1049 incident = incident.with_affected_subject(user);
1050 }
1051 if let Some(ref ip) = event.ip_address {
1052 incident = incident.with_involved_ip(ip);
1053 }
1054
1055 self.create_and_notify_incident(incident)
1056 }
1057
1058 fn check_mass_data_operation(
1060 &self,
1061 event: &SecurityEvent,
1062 is_deletion: bool,
1063 ) -> Option<BreachIncident> {
1064 let severity = if is_deletion {
1065 BreachSeverity::Critical
1066 } else {
1067 BreachSeverity::High
1068 };
1069
1070 let mut incident =
1071 BreachIncident::new(event.event_type, severity, &event.description, &self.incident_counter)
1072 .with_related_event(&event.id);
1073
1074 if let Some(ref user) = event.user {
1075 incident = incident.with_affected_subject(user);
1076 }
1077 if let Some(ref resource) = event.resource {
1078 incident = incident.with_detail("resource", resource);
1079 }
1080
1081 self.create_and_notify_incident(incident)
1082 }
1083
1084 fn check_sql_injection(&self, event: &SecurityEvent) -> Option<BreachIncident> {
1086 let config = self.config.read();
1087 if !config.enable_sql_injection_detection {
1088 return None;
1089 }
1090 drop(config);
1091
1092 let mut incident = BreachIncident::new(
1093 SecurityEventType::SqlInjection,
1094 BreachSeverity::High,
1095 &event.description,
1096 &self.incident_counter,
1097 )
1098 .with_related_event(&event.id);
1099
1100 if let Some(ref user) = event.user {
1101 incident = incident.with_affected_subject(user);
1102 }
1103 if let Some(ref ip) = event.ip_address {
1104 incident = incident.with_involved_ip(ip);
1105 }
1106
1107 self.create_and_notify_incident(incident)
1108 }
1109
1110 fn create_high_severity_incident(&self, event: &SecurityEvent) -> Option<BreachIncident> {
1112 let mut incident =
1113 BreachIncident::new(event.event_type, BreachSeverity::High, &event.description, &self.incident_counter)
1114 .with_related_event(&event.id);
1115
1116 if let Some(ref user) = event.user {
1117 incident = incident.with_affected_subject(user);
1118 }
1119 if let Some(ref ip) = event.ip_address {
1120 incident = incident.with_involved_ip(ip);
1121 }
1122
1123 self.create_and_notify_incident(incident)
1124 }
1125
1126 fn create_critical_incident(&self, event: &SecurityEvent) -> Option<BreachIncident> {
1128 let mut incident = BreachIncident::new(
1129 event.event_type,
1130 BreachSeverity::Critical,
1131 &event.description,
1132 &self.incident_counter,
1133 )
1134 .with_related_event(&event.id);
1135
1136 if let Some(ref user) = event.user {
1137 incident = incident.with_affected_subject(user);
1138 }
1139 if let Some(ref ip) = event.ip_address {
1140 incident = incident.with_involved_ip(ip);
1141 }
1142
1143 self.create_and_notify_incident(incident)
1144 }
1145
1146 fn create_and_notify_incident(&self, mut incident: BreachIncident) -> Option<BreachIncident> {
1148 let notifiers = self.notifiers.read();
1150 let now = now_timestamp();
1151
1152 for notifier in notifiers.iter() {
1153 match notifier.notify(&incident) {
1154 Ok(()) => {
1155 incident
1156 .notification_timestamps
1157 .insert(notifier.name().to_string(), now);
1158 tracing::info!(
1159 "Breach notification sent via {}: {}",
1160 notifier.name(),
1161 incident.id
1162 );
1163 }
1164 Err(e) => {
1165 tracing::error!(
1166 "Failed to send breach notification via {}: {}",
1167 notifier.name(),
1168 e
1169 );
1170 }
1171 }
1172 }
1173 drop(notifiers);
1174
1175 incident.notified = !incident.notification_timestamps.is_empty();
1176
1177 {
1179 let mut incidents = self.incidents.write();
1180 while incidents.len() >= MAX_INCIDENTS_IN_MEMORY {
1181 incidents.pop_front();
1182 }
1183 incidents.push_back(incident.clone());
1184 }
1185
1186 self.flush_incidents_to_disk();
1188
1189 tracing::warn!(
1190 "Breach incident detected: {} (severity: {})",
1191 incident.id,
1192 incident.severity
1193 );
1194
1195 Some(incident)
1196 }
1197
1198 pub fn list_incidents(&self) -> Vec<BreachIncident> {
1200 self.incidents.read().iter().cloned().collect()
1201 }
1202
1203 pub fn list_events(&self, event_type: Option<&str>, limit: usize) -> Vec<SecurityEvent> {
1205 let events = self.events.read();
1206
1207 let filter_type = event_type.and_then(|t| match t {
1208 "failed_login" => Some(SecurityEventType::FailedLogin),
1209 "unauthorized_access" => Some(SecurityEventType::UnauthorizedAccess),
1210 "unusual_access_pattern" => Some(SecurityEventType::UnusualAccessPattern),
1211 "admin_from_unknown_ip" => Some(SecurityEventType::AdminFromUnknownIp),
1212 "mass_data_export" => Some(SecurityEventType::MassDataExport),
1213 "mass_data_deletion" => Some(SecurityEventType::MassDataDeletion),
1214 "session_hijacking" => Some(SecurityEventType::SessionHijacking),
1215 "sql_injection" => Some(SecurityEventType::SqlInjection),
1216 "brute_force_attack" => Some(SecurityEventType::BruteForceAttack),
1217 "privilege_escalation" => Some(SecurityEventType::PrivilegeEscalation),
1218 _ => None,
1219 });
1220
1221 let mut all_events: Vec<SecurityEvent> = if let Some(filter) = filter_type {
1222 events
1223 .get(&filter)
1224 .map(|q| q.iter().map(|r| r.event.clone()).collect())
1225 .unwrap_or_default()
1226 } else {
1227 events
1228 .values()
1229 .flat_map(|q| q.iter().map(|r| r.event.clone()))
1230 .collect()
1231 };
1232
1233 all_events.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
1235 all_events.truncate(limit);
1236 all_events
1237 }
1238
1239 pub fn get_incidents(
1241 &self,
1242 status: Option<IncidentStatus>,
1243 severity: Option<BreachSeverity>,
1244 limit: usize,
1245 ) -> Vec<BreachIncident> {
1246 let incidents = self.incidents.read();
1247 incidents
1248 .iter()
1249 .rev()
1250 .filter(|i| status.is_none() || Some(i.status) == status)
1251 .filter(|i| severity.is_none() || Some(i.severity) == severity)
1252 .take(limit)
1253 .cloned()
1254 .collect()
1255 }
1256
1257 pub fn get_incident(&self, id: &str) -> Option<BreachIncident> {
1259 self.incidents.read().iter().find(|i| i.id == id).cloned()
1260 }
1261
1262 pub fn acknowledge_incident(&self, id: &str, acknowledged_by: &str) -> Option<BreachIncident> {
1264 let mut incidents = self.incidents.write();
1265 for incident in incidents.iter_mut() {
1266 if incident.id == id {
1267 incident.status = IncidentStatus::Acknowledged;
1268 incident.acknowledged_by = Some(acknowledged_by.to_string());
1269 incident.acknowledged_at = Some(now_timestamp());
1270 return Some(incident.clone());
1271 }
1272 }
1273 None
1274 }
1275
1276 pub fn resolve_incident(
1278 &self,
1279 id: &str,
1280 resolution_notes: &str,
1281 false_positive: bool,
1282 ) -> Option<BreachIncident> {
1283 let mut incidents = self.incidents.write();
1284 for incident in incidents.iter_mut() {
1285 if incident.id == id {
1286 incident.status = if false_positive {
1287 IncidentStatus::FalsePositive
1288 } else {
1289 IncidentStatus::Resolved
1290 };
1291 incident.resolution_notes = Some(resolution_notes.to_string());
1292 incident.resolved_at = Some(now_timestamp());
1293 return Some(incident.clone());
1294 }
1295 }
1296 None
1297 }
1298
1299 pub fn generate_report(&self, id: &str) -> Option<IncidentReport> {
1301 let incident = self.get_incident(id)?;
1302
1303 let events = self.events.read();
1305 let related_events: Vec<SecurityEvent> = events
1306 .values()
1307 .flat_map(|q| q.iter())
1308 .filter(|r| incident.related_events.contains(&r.event.id))
1309 .map(|r| r.event.clone())
1310 .collect();
1311
1312 Some(IncidentReport {
1313 incident,
1314 related_events,
1315 generated_at: now_timestamp(),
1316 generated_at_formatted: format_timestamp(now_timestamp()),
1317 })
1318 }
1319
1320 pub fn get_stats(&self) -> BreachStats {
1322 let incidents = self.incidents.read();
1323
1324 let mut stats = BreachStats {
1325 total_incidents: incidents.len(),
1326 open_incidents: 0,
1327 acknowledged_incidents: 0,
1328 resolved_incidents: 0,
1329 false_positives: 0,
1330 by_severity: HashMap::new(),
1331 by_type: HashMap::new(),
1332 };
1333
1334 for incident in incidents.iter() {
1335 match incident.status {
1336 IncidentStatus::Open => stats.open_incidents += 1,
1337 IncidentStatus::Acknowledged | IncidentStatus::Investigating => {
1338 stats.acknowledged_incidents += 1
1339 }
1340 IncidentStatus::Resolved => stats.resolved_incidents += 1,
1341 IncidentStatus::FalsePositive => stats.false_positives += 1,
1342 }
1343
1344 *stats
1345 .by_severity
1346 .entry(incident.severity.to_string())
1347 .or_insert(0) += 1;
1348 *stats
1349 .by_type
1350 .entry(incident.incident_type.to_string())
1351 .or_insert(0) += 1;
1352 }
1353
1354 stats
1355 }
1356
1357 pub fn cleanup(&self) {
1359 let now = Instant::now();
1360 let retention = Duration::from_secs(24 * 60 * 60); {
1364 let mut events = self.events.write();
1365 for queue in events.values_mut() {
1366 while let Some(front) = queue.front() {
1367 if now.duration_since(front.received_at) > retention {
1368 queue.pop_front();
1369 } else {
1370 break;
1371 }
1372 }
1373 }
1374 }
1375
1376 {
1378 let mut logins = self.failed_logins.write();
1379 let config = self.config.read();
1380 let window = Duration::from_secs(config.failed_login_window_secs);
1381 drop(config);
1382
1383 for queue in logins.values_mut() {
1384 while let Some(front) = queue.front() {
1385 if now.duration_since(*front) > window {
1386 queue.pop_front();
1387 } else {
1388 break;
1389 }
1390 }
1391 }
1392 logins.retain(|_, v| !v.is_empty());
1393 }
1394
1395 {
1397 let mut patterns = self.access_patterns.write();
1398 let config = self.config.read();
1399 let window = Duration::from_secs(config.unusual_access_window_secs);
1400 drop(config);
1401
1402 for queue in patterns.values_mut() {
1403 while let Some(front) = queue.front() {
1404 if now.duration_since(*front) > window {
1405 queue.pop_front();
1406 } else {
1407 break;
1408 }
1409 }
1410 }
1411 patterns.retain(|_, v| !v.is_empty());
1412 }
1413 }
1414}
1415
1416impl Default for BreachDetector {
1417 fn default() -> Self {
1418 Self::new()
1419 }
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1428pub struct IncidentReport {
1429 pub incident: BreachIncident,
1431 pub related_events: Vec<SecurityEvent>,
1433 pub generated_at: u64,
1435 pub generated_at_formatted: String,
1437}
1438
1439#[derive(Debug, Clone, Serialize, Deserialize)]
1445pub struct BreachStats {
1446 pub total_incidents: usize,
1448 pub open_incidents: usize,
1450 pub acknowledged_incidents: usize,
1452 pub resolved_incidents: usize,
1454 pub false_positives: usize,
1456 pub by_severity: HashMap<String, usize>,
1458 pub by_type: HashMap<String, usize>,
1460}
1461
1462fn now_timestamp() -> u64 {
1468 SystemTime::now()
1469 .duration_since(UNIX_EPOCH)
1470 .unwrap_or_default()
1471 .as_millis() as u64
1472}
1473
1474fn generate_event_id(counter: &AtomicU64) -> String {
1476 format!("evt-{:012}", counter.fetch_add(1, Ordering::SeqCst))
1477}
1478
1479fn generate_incident_id(counter: &AtomicU64) -> String {
1481 format!("inc-{:012}", counter.fetch_add(1, Ordering::SeqCst))
1482}
1483
1484fn format_timestamp(timestamp_ms: u64) -> String {
1486 let secs = timestamp_ms / 1000;
1487 let datetime = UNIX_EPOCH + Duration::from_secs(secs);
1488 let duration = datetime.duration_since(UNIX_EPOCH).unwrap_or_default();
1489 let total_secs = duration.as_secs();
1490
1491 let days_since_epoch = total_secs / 86400;
1492 let secs_today = total_secs % 86400;
1493
1494 let hours = secs_today / 3600;
1495 let minutes = (secs_today % 3600) / 60;
1496 let seconds = secs_today % 60;
1497
1498 let mut year = 1970u64;
1499 let mut remaining_days = days_since_epoch;
1500
1501 loop {
1502 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
1503 if remaining_days < days_in_year {
1504 break;
1505 }
1506 remaining_days -= days_in_year;
1507 year += 1;
1508 }
1509
1510 let days_in_months: [u64; 12] = if is_leap_year(year) {
1511 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
1512 } else {
1513 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
1514 };
1515
1516 let mut month = 1u64;
1517 for &days in &days_in_months {
1518 if remaining_days < days {
1519 break;
1520 }
1521 remaining_days -= days;
1522 month += 1;
1523 }
1524 let day = remaining_days + 1;
1525
1526 format!(
1527 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
1528 year, month, day, hours, minutes, seconds
1529 )
1530}
1531
1532fn is_leap_year(year: u64) -> bool {
1533 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
1534}
1535
1536#[derive(Debug, Deserialize)]
1542pub struct AcknowledgeRequest {
1543 pub acknowledged_by: String,
1544}
1545
1546#[derive(Debug, Deserialize)]
1548pub struct ResolveRequest {
1549 pub resolution_notes: String,
1550 #[serde(default)]
1551 pub false_positive: bool,
1552}
1553
1554#[derive(Debug, Deserialize)]
1556pub struct ListBreachesQuery {
1557 pub status: Option<String>,
1558 pub severity: Option<String>,
1559 #[serde(default = "default_limit")]
1560 pub limit: usize,
1561}
1562
1563fn default_limit() -> usize {
1564 100
1565}
1566
1567#[derive(Debug, Deserialize)]
1569pub struct ListEventsQuery {
1570 #[serde(default = "default_limit")]
1571 pub limit: usize,
1572 pub event_type: Option<String>,
1573}
1574
1575use crate::state::AppState;
1580use axum::{
1581 extract::{Path, Query, State},
1582 http::StatusCode,
1583 response::IntoResponse,
1584 Json,
1585};
1586
1587#[derive(Debug, serde::Serialize)]
1589pub struct ListBreachesResponse {
1590 pub incidents: Vec<BreachIncident>,
1591 pub total: usize,
1592 pub stats: BreachStats,
1593}
1594
1595#[derive(Debug, serde::Serialize)]
1597pub struct SecurityEventsResponse {
1598 pub events: Vec<SecurityEvent>,
1599 pub total: usize,
1600}
1601
1602pub async fn list_breaches(
1605 State(state): State<AppState>,
1606 Query(params): Query<ListBreachesQuery>,
1607) -> Json<ListBreachesResponse> {
1608 let status = params.status.as_ref().and_then(|s| match s.as_str() {
1609 "open" => Some(IncidentStatus::Open),
1610 "acknowledged" => Some(IncidentStatus::Acknowledged),
1611 "investigating" => Some(IncidentStatus::Investigating),
1612 "resolved" => Some(IncidentStatus::Resolved),
1613 "false_positive" => Some(IncidentStatus::FalsePositive),
1614 _ => None,
1615 });
1616
1617 let severity = params.severity.as_ref().and_then(|s| match s.as_str() {
1618 "low" => Some(BreachSeverity::Low),
1619 "medium" => Some(BreachSeverity::Medium),
1620 "high" => Some(BreachSeverity::High),
1621 "critical" => Some(BreachSeverity::Critical),
1622 _ => None,
1623 });
1624
1625 let incidents = state
1626 .breach_detector
1627 .get_incidents(status, severity, params.limit);
1628 let total = incidents.len();
1629 let stats = state.breach_detector.get_stats();
1630
1631 Json(ListBreachesResponse {
1632 incidents,
1633 total,
1634 stats,
1635 })
1636}
1637
1638pub async fn get_breach(
1641 State(state): State<AppState>,
1642 Path(id): Path<String>,
1643) -> impl IntoResponse {
1644 match state.breach_detector.get_incident(&id) {
1645 Some(incident) => (
1646 StatusCode::OK,
1647 Json(serde_json::json!({
1648 "success": true,
1649 "incident": incident,
1650 })),
1651 ),
1652 None => (
1653 StatusCode::NOT_FOUND,
1654 Json(serde_json::json!({
1655 "success": false,
1656 "error": format!("Incident '{}' not found", id),
1657 })),
1658 ),
1659 }
1660}
1661
1662pub async fn acknowledge_breach(
1665 State(state): State<AppState>,
1666 Path(id): Path<String>,
1667 Json(request): Json<AcknowledgeRequest>,
1668) -> impl IntoResponse {
1669 match state
1670 .breach_detector
1671 .acknowledge_incident(&id, &request.acknowledged_by)
1672 {
1673 Some(incident) => {
1674 tracing::info!(
1675 "Breach incident {} acknowledged by {}",
1676 id,
1677 request.acknowledged_by
1678 );
1679 (
1680 StatusCode::OK,
1681 Json(serde_json::json!({
1682 "success": true,
1683 "incident": incident,
1684 "message": "Incident acknowledged successfully",
1685 })),
1686 )
1687 }
1688 None => (
1689 StatusCode::NOT_FOUND,
1690 Json(serde_json::json!({
1691 "success": false,
1692 "error": format!("Incident '{}' not found", id),
1693 })),
1694 ),
1695 }
1696}
1697
1698pub async fn resolve_breach(
1701 State(state): State<AppState>,
1702 Path(id): Path<String>,
1703 Json(request): Json<ResolveRequest>,
1704) -> impl IntoResponse {
1705 match state.breach_detector.resolve_incident(
1706 &id,
1707 &request.resolution_notes,
1708 request.false_positive,
1709 ) {
1710 Some(incident) => {
1711 let status_str = if request.false_positive {
1712 "false positive"
1713 } else {
1714 "resolved"
1715 };
1716 tracing::info!("Breach incident {} marked as {}", id, status_str);
1717 (
1718 StatusCode::OK,
1719 Json(serde_json::json!({
1720 "success": true,
1721 "incident": incident,
1722 "message": format!("Incident marked as {}", status_str),
1723 })),
1724 )
1725 }
1726 None => (
1727 StatusCode::NOT_FOUND,
1728 Json(serde_json::json!({
1729 "success": false,
1730 "error": format!("Incident '{}' not found", id),
1731 })),
1732 ),
1733 }
1734}
1735
1736pub async fn get_breach_report(
1739 State(state): State<AppState>,
1740 Path(id): Path<String>,
1741) -> impl IntoResponse {
1742 match state.breach_detector.generate_report(&id) {
1743 Some(report) => (
1744 StatusCode::OK,
1745 Json(serde_json::json!({
1746 "success": true,
1747 "report": report,
1748 })),
1749 ),
1750 None => (
1751 StatusCode::NOT_FOUND,
1752 Json(serde_json::json!({
1753 "success": false,
1754 "error": format!("Incident '{}' not found", id),
1755 })),
1756 ),
1757 }
1758}
1759
1760pub async fn list_security_events(
1763 State(state): State<AppState>,
1764 Query(params): Query<ListEventsQuery>,
1765) -> Json<SecurityEventsResponse> {
1766 let events = state
1767 .breach_detector
1768 .list_events(params.event_type.as_deref(), params.limit);
1769 let total = events.len();
1770
1771 Json(SecurityEventsResponse { events, total })
1772}
1773
1774pub async fn get_breach_stats(State(state): State<AppState>) -> Json<BreachStats> {
1777 Json(state.breach_detector.get_stats())
1778}
1779
1780pub async fn trigger_cleanup(State(state): State<AppState>) -> Json<serde_json::Value> {
1783 state.breach_detector.cleanup();
1784 Json(serde_json::json!({
1785 "success": true,
1786 "message": "Cleanup completed successfully",
1787 }))
1788}
1789
1790#[cfg(test)]
1795mod tests {
1796 use super::*;
1797
1798 #[test]
1799 fn test_security_event_creation() {
1800 let counter = AtomicU64::new(1);
1801 let event = SecurityEvent::new(SecurityEventType::FailedLogin, "Test failed login", &counter)
1802 .with_user("testuser")
1803 .with_ip("192.168.1.1");
1804
1805 assert!(event.id.starts_with("evt-"));
1806 assert_eq!(event.event_type, SecurityEventType::FailedLogin);
1807 assert_eq!(event.user, Some("testuser".to_string()));
1808 assert_eq!(event.ip_address, Some("192.168.1.1".to_string()));
1809 }
1810
1811 #[test]
1812 fn test_breach_incident_creation() {
1813 let counter = AtomicU64::new(1);
1814 let incident = BreachIncident::new(
1815 SecurityEventType::FailedLogin,
1816 BreachSeverity::Medium,
1817 "Multiple failed logins detected",
1818 &counter,
1819 )
1820 .with_affected_subject("user1")
1821 .with_involved_ip("10.0.0.1");
1822
1823 assert!(incident.id.starts_with("inc-"));
1824 assert_eq!(incident.severity, BreachSeverity::Medium);
1825 assert!(incident.affected_subjects.contains(&"user1".to_string()));
1826 assert!(incident.involved_ips.contains(&"10.0.0.1".to_string()));
1827 }
1828
1829 #[test]
1830 fn test_breach_detector_failed_login_detection() {
1831 let config = DetectionConfig {
1832 failed_login_threshold: 3,
1833 failed_login_window_secs: 300,
1834 ..Default::default()
1835 };
1836 let detector = BreachDetector::with_config(config);
1837
1838 assert!(detector
1840 .record_failed_login("user1", Some("192.168.1.1"))
1841 .is_none());
1842 assert!(detector
1843 .record_failed_login("user1", Some("192.168.1.1"))
1844 .is_none());
1845
1846 let incident = detector.record_failed_login("user1", Some("192.168.1.1"));
1848 assert!(incident.is_some());
1849
1850 let incident = incident.unwrap();
1851 assert_eq!(incident.incident_type, SecurityEventType::FailedLogin);
1852 assert!(incident.affected_subjects.contains(&"user1".to_string()));
1853 }
1854
1855 #[test]
1856 fn test_breach_detector_unauthorized_access() {
1857 let detector = BreachDetector::new();
1858
1859 let incident =
1860 detector.record_unauthorized_access("user1", "admin/users", "write", Some("10.0.0.1"));
1861
1862 assert!(incident.is_some());
1863 let incident = incident.unwrap();
1864 assert_eq!(
1865 incident.incident_type,
1866 SecurityEventType::UnauthorizedAccess
1867 );
1868 }
1869
1870 #[test]
1871 fn test_breach_detector_mass_data_operation() {
1872 let config = DetectionConfig {
1873 mass_data_threshold: 100,
1874 ..Default::default()
1875 };
1876 let detector = BreachDetector::with_config(config);
1877
1878 assert!(detector
1880 .record_mass_data_operation("user1", "users", 50, false)
1881 .is_none());
1882
1883 let incident = detector.record_mass_data_operation("user1", "users", 1000, true);
1885 assert!(incident.is_some());
1886
1887 let incident = incident.unwrap();
1888 assert_eq!(incident.incident_type, SecurityEventType::MassDataDeletion);
1889 assert_eq!(incident.severity, BreachSeverity::Critical);
1890 }
1891
1892 #[test]
1893 fn test_breach_detector_admin_unknown_ip() {
1894 let config = DetectionConfig {
1895 trusted_admin_ips: vec!["127.0.0.1".to_string()],
1896 ..Default::default()
1897 };
1898 let detector = BreachDetector::with_config(config);
1899
1900 assert!(detector
1902 .record_admin_action("admin", "delete_user", "127.0.0.1")
1903 .is_none());
1904
1905 let incident = detector.record_admin_action("admin", "delete_user", "192.168.1.100");
1907 assert!(incident.is_some());
1908
1909 let incident = incident.unwrap();
1910 assert_eq!(
1911 incident.incident_type,
1912 SecurityEventType::AdminFromUnknownIp
1913 );
1914 assert_eq!(incident.severity, BreachSeverity::High);
1915 }
1916
1917 #[test]
1918 fn test_incident_acknowledge_and_resolve() {
1919 let detector = BreachDetector::new();
1920
1921 let incident =
1923 detector.record_unauthorized_access("user1", "admin/users", "write", Some("10.0.0.1"));
1924 let incident = incident.expect("should create incident");
1925
1926 let acknowledged = detector.acknowledge_incident(&incident.id, "admin");
1928 assert!(acknowledged.is_some());
1929 let acknowledged = acknowledged.unwrap();
1930 assert_eq!(acknowledged.status, IncidentStatus::Acknowledged);
1931 assert_eq!(acknowledged.acknowledged_by, Some("admin".to_string()));
1932
1933 let resolved = detector.resolve_incident(&incident.id, "Investigated and addressed", false);
1935 assert!(resolved.is_some());
1936 let resolved = resolved.unwrap();
1937 assert_eq!(resolved.status, IncidentStatus::Resolved);
1938 }
1939
1940 #[test]
1941 fn test_incident_report_generation() {
1942 let detector = BreachDetector::new();
1943
1944 let incident =
1945 detector.record_unauthorized_access("user1", "admin/users", "write", Some("10.0.0.1"));
1946 let incident = incident.expect("should create incident");
1947
1948 let report = detector.generate_report(&incident.id);
1949 assert!(report.is_some());
1950
1951 let report = report.unwrap();
1952 assert_eq!(report.incident.id, incident.id);
1953 }
1954
1955 #[test]
1956 fn test_breach_stats() {
1957 let detector = BreachDetector::new();
1958
1959 detector.record_unauthorized_access("user1", "admin", "write", Some("10.0.0.1"));
1961 detector.record_unauthorized_access("user2", "admin", "write", Some("10.0.0.2"));
1962
1963 let stats = detector.get_stats();
1964 assert_eq!(stats.total_incidents, 2);
1965 assert_eq!(stats.open_incidents, 2);
1966 }
1967
1968 #[test]
1969 fn test_severity_ordering() {
1970 assert!(BreachSeverity::Low < BreachSeverity::Medium);
1971 assert!(BreachSeverity::Medium < BreachSeverity::High);
1972 assert!(BreachSeverity::High < BreachSeverity::Critical);
1973 }
1974
1975 #[test]
1976 fn test_requires_immediate_notification() {
1977 let counter = AtomicU64::new(1);
1978 let low_incident =
1979 BreachIncident::new(SecurityEventType::FailedLogin, BreachSeverity::Low, "test", &counter);
1980 assert!(!low_incident.requires_immediate_notification());
1981
1982 let high_incident = BreachIncident::new(
1983 SecurityEventType::MassDataDeletion,
1984 BreachSeverity::High,
1985 "test",
1986 &counter,
1987 );
1988 assert!(high_incident.requires_immediate_notification());
1989
1990 let critical_incident = BreachIncident::new(
1991 SecurityEventType::BruteForceAttack,
1992 BreachSeverity::Critical,
1993 "test",
1994 &counter,
1995 );
1996 assert!(critical_incident.requires_immediate_notification());
1997 }
1998}