1use std::collections::{HashMap, VecDeque};
32
33const FNV1A_OFFSET: u64 = 14_695_981_039_346_656_037;
39const FNV1A_PRIME: u64 = 1_099_511_628_211;
41
42fn fnv1a(data: &[u8]) -> u64 {
44 let mut hash = FNV1A_OFFSET;
45 for &byte in data {
46 hash ^= byte as u64;
47 hash = hash.wrapping_mul(FNV1A_PRIME);
48 }
49 hash
50}
51
52fn compute_event_id(peer_id: &str, threat_type_name: &str, timestamp: u64) -> u64 {
54 let mut raw = String::with_capacity(
55 peer_id.len() + threat_type_name.len() + 20, );
57 raw.push_str(peer_id);
58 raw.push_str(threat_type_name);
59 raw.push_str(×tamp.to_string());
60 fnv1a(raw.as_bytes())
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub enum ThreatType {
70 Sybil,
72 Eclipse,
74 DDoS,
76 ManInTheMiddle,
78 ReplayAttack,
80 RoutingPoison,
82 DataTampering,
84}
85
86impl ThreatType {
87 pub fn name(&self) -> &'static str {
89 match self {
90 ThreatType::Sybil => "Sybil",
91 ThreatType::Eclipse => "Eclipse",
92 ThreatType::DDoS => "DDoS",
93 ThreatType::ManInTheMiddle => "ManInTheMiddle",
94 ThreatType::ReplayAttack => "ReplayAttack",
95 ThreatType::RoutingPoison => "RoutingPoison",
96 ThreatType::DataTampering => "DataTampering",
97 }
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ThreatLevel {
108 None,
110 Low,
112 Medium,
114 High,
116 Critical,
118}
119
120impl ThreatLevel {
121 pub fn score_contribution(self) -> f64 {
124 match self {
125 ThreatLevel::None => 0.0,
126 ThreatLevel::Low => 5.0,
127 ThreatLevel::Medium => 15.0,
128 ThreatLevel::High => 30.0,
129 ThreatLevel::Critical => 50.0,
130 }
131 }
132
133 fn rank(self) -> u8 {
135 match self {
136 ThreatLevel::None => 0,
137 ThreatLevel::Low => 1,
138 ThreatLevel::Medium => 2,
139 ThreatLevel::High => 3,
140 ThreatLevel::Critical => 4,
141 }
142 }
143}
144
145impl PartialOrd for ThreatLevel {
146 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
147 Some(self.cmp(other))
148 }
149}
150
151impl Ord for ThreatLevel {
152 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
153 self.rank().cmp(&other.rank())
154 }
155}
156
157#[derive(Debug, Clone)]
163pub struct SecurityEvent {
164 pub id: u64,
166 pub peer_id: String,
168 pub threat_type: ThreatType,
170 pub threat_level: ThreatLevel,
172 pub description: String,
174 pub timestamp: u64,
176 pub evidence: Vec<String>,
178}
179
180impl SecurityEvent {
181 pub fn new(
183 peer_id: String,
184 threat_type: ThreatType,
185 threat_level: ThreatLevel,
186 description: String,
187 timestamp: u64,
188 evidence: Vec<String>,
189 ) -> Self {
190 let id = compute_event_id(&peer_id, threat_type.name(), timestamp);
191 SecurityEvent {
192 id,
193 peer_id,
194 threat_type,
195 threat_level,
196 description,
197 timestamp,
198 evidence,
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
209pub struct ThreatScore {
210 pub peer_id: String,
212 pub score: f64,
214 pub contributing_events: u32,
216 pub last_updated: u64,
218}
219
220impl ThreatScore {
221 pub fn new(peer_id: String, now: u64) -> Self {
223 ThreatScore {
224 peer_id,
225 score: 0.0,
226 contributing_events: 0,
227 last_updated: now,
228 }
229 }
230
231 pub fn add(&mut self, points: f64, now: u64) {
233 self.score = (self.score + points).min(100.0);
234 self.contributing_events = self.contributing_events.saturating_add(1);
235 self.last_updated = now;
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum IncidentStatus {
246 Open,
248 Investigating,
250 Resolved,
252 FalsePositive,
254}
255
256#[derive(Debug, Clone)]
262pub struct SecurityIncident {
263 pub id: u64,
265 pub events: Vec<u64>,
267 pub status: IncidentStatus,
269 pub created_at: u64,
271 pub resolved_at: Option<u64>,
273}
274
275#[derive(Debug, Clone)]
281pub struct SecurityMonitorStats {
282 pub total_events: usize,
284 pub open_incidents: usize,
286 pub resolved_incidents: usize,
288 pub high_threat_peers: usize,
290 pub avg_threat_score: f64,
292}
293
294pub struct NetworkSecurityMonitor {
304 events: VecDeque<SecurityEvent>,
306 scores: HashMap<String, ThreatScore>,
308 incidents: Vec<SecurityIncident>,
310 max_events: usize,
312 threat_threshold: f64,
314 next_id: u64,
316}
317
318impl NetworkSecurityMonitor {
319 pub fn new(max_events: usize, threat_threshold: f64) -> Self {
331 let threshold = if threat_threshold <= 0.0 {
332 50.0
333 } else {
334 threat_threshold
335 };
336 NetworkSecurityMonitor {
337 events: VecDeque::with_capacity(max_events.min(4096)),
338 scores: HashMap::new(),
339 incidents: Vec::new(),
340 max_events,
341 threat_threshold: threshold,
342 next_id: 1,
343 }
344 }
345
346 pub fn default_monitor() -> Self {
348 Self::new(10_000, 50.0)
349 }
350
351 pub fn record_event(
360 &mut self,
361 peer_id: String,
362 threat_type: ThreatType,
363 level: ThreatLevel,
364 description: String,
365 evidence: Vec<String>,
366 now: u64,
367 ) -> u64 {
368 let event = SecurityEvent::new(
370 peer_id.clone(),
371 threat_type,
372 level,
373 description,
374 now,
375 evidence,
376 );
377 let event_id = event.id;
378
379 if self.events.len() >= self.max_events {
381 self.events.pop_front();
382 }
383 self.events.push_back(event);
384
385 let points = level.score_contribution();
387 let score_entry = self
388 .scores
389 .entry(peer_id.clone())
390 .or_insert_with(|| ThreatScore::new(peer_id.clone(), now));
391
392 Self::apply_decay_inner(score_entry, now);
394 score_entry.add(points, now);
395
396 let current_score = score_entry.score;
397
398 if current_score >= self.threat_threshold && !self.has_open_incident_for_peer(&peer_id) {
401 let peer_events: Vec<u64> = self
402 .events
403 .iter()
404 .filter(|e| e.peer_id == peer_id)
405 .map(|e| e.id)
406 .collect();
407 self.create_incident(peer_events, now);
408 }
409
410 event_id
411 }
412
413 pub fn threat_score(&mut self, peer_id: &str, now: u64) -> f64 {
420 match self.scores.get_mut(peer_id) {
421 None => 0.0,
422 Some(score) => {
423 Self::apply_decay_inner(score, now);
424 score.score
425 }
426 }
427 }
428
429 pub fn apply_decay(score: &mut ThreatScore, now: u64) {
437 Self::apply_decay_inner(score, now);
438 }
439
440 fn apply_decay_inner(score: &mut ThreatScore, now: u64) {
441 if now <= score.last_updated {
442 return;
443 }
444 let elapsed_ms = now - score.last_updated;
445 let hours = elapsed_ms / 3_600_000;
446 if hours == 0 {
447 return;
448 }
449 let factor = 0.9_f64.powi(hours as i32);
451 score.score *= factor;
452 score.last_updated = now;
453 }
454
455 pub fn top_threats(&mut self, n: usize, now: u64) -> Vec<&ThreatScore> {
462 for score in self.scores.values_mut() {
464 Self::apply_decay_inner(score, now);
465 }
466 let mut refs: Vec<&ThreatScore> = self.scores.values().collect();
467 refs.sort_by(|a, b| {
468 b.score
469 .partial_cmp(&a.score)
470 .unwrap_or(std::cmp::Ordering::Equal)
471 });
472 refs.truncate(n);
473 refs
474 }
475
476 pub fn events_for_peer(&self, peer_id: &str) -> Vec<&SecurityEvent> {
478 self.events
479 .iter()
480 .filter(|e| e.peer_id == peer_id)
481 .collect()
482 }
483
484 pub fn events_by_threat_type(&self, threat_type: &ThreatType) -> Vec<&SecurityEvent> {
486 self.events
487 .iter()
488 .filter(|e| &e.threat_type == threat_type)
489 .collect()
490 }
491
492 pub fn events_since(&self, timestamp: u64) -> Vec<&SecurityEvent> {
494 self.events
495 .iter()
496 .filter(|e| e.timestamp >= timestamp)
497 .collect()
498 }
499
500 pub fn create_incident(&mut self, events: Vec<u64>, now: u64) -> u64 {
506 let id = self.next_incident_id(now, events.len() as u64);
507 let incident = SecurityIncident {
508 id,
509 events,
510 status: IncidentStatus::Open,
511 created_at: now,
512 resolved_at: None,
513 };
514 self.incidents.push(incident);
515 id
516 }
517
518 pub fn update_incident_status(&mut self, incident_id: u64, status: IncidentStatus) -> bool {
520 match self.incidents.iter_mut().find(|i| i.id == incident_id) {
521 None => false,
522 Some(incident) => {
523 incident.status = status;
524 true
525 }
526 }
527 }
528
529 pub fn open_incidents(&self) -> Vec<&SecurityIncident> {
531 self.incidents
532 .iter()
533 .filter(|i| {
534 i.status == IncidentStatus::Open || i.status == IncidentStatus::Investigating
535 })
536 .collect()
537 }
538
539 pub fn clear_peer_history(&mut self, peer_id: &str) -> usize {
546 let before = self.events.len();
547 self.events.retain(|e| e.peer_id != peer_id);
548 let removed = before - self.events.len();
549 self.scores.remove(peer_id);
550 removed
551 }
552
553 pub fn stats(&mut self, now: u64) -> SecurityMonitorStats {
560 for score in self.scores.values_mut() {
562 Self::apply_decay_inner(score, now);
563 }
564
565 let total_events = self.events.len();
566 let open_incidents = self.open_incidents().len();
567 let resolved_incidents = self
568 .incidents
569 .iter()
570 .filter(|i| {
571 i.status == IncidentStatus::Resolved || i.status == IncidentStatus::FalsePositive
572 })
573 .count();
574
575 let high_threat_peers = self.scores.values().filter(|s| s.score >= 50.0).count();
576
577 let avg_threat_score = if self.scores.is_empty() {
578 0.0
579 } else {
580 let sum: f64 = self.scores.values().map(|s| s.score).sum();
581 sum / self.scores.len() as f64
582 };
583
584 SecurityMonitorStats {
585 total_events,
586 open_incidents,
587 resolved_incidents,
588 high_threat_peers,
589 avg_threat_score,
590 }
591 }
592
593 fn has_open_incident_for_peer(&self, peer_id: &str) -> bool {
600 let peer_event_ids: std::collections::HashSet<u64> = self
602 .events
603 .iter()
604 .filter(|e| e.peer_id == peer_id)
605 .map(|e| e.id)
606 .collect();
607
608 self.incidents.iter().any(|inc| {
609 (inc.status == IncidentStatus::Open || inc.status == IncidentStatus::Investigating)
610 && inc.events.iter().any(|eid| peer_event_ids.contains(eid))
611 })
612 }
613
614 fn next_incident_id(&mut self, now: u64, extra: u64) -> u64 {
616 let counter = self.next_id;
617 self.next_id = self.next_id.wrapping_add(1);
618 let mut data = [0u8; 24];
619 data[0..8].copy_from_slice(&counter.to_le_bytes());
620 data[8..16].copy_from_slice(&now.to_le_bytes());
621 data[16..24].copy_from_slice(&extra.to_le_bytes());
622 fnv1a(&data)
623 }
624}
625
626#[cfg(test)]
631mod tests {
632 use crate::security_monitor::{
633 IncidentStatus, NetworkSecurityMonitor, SecurityEvent, ThreatLevel, ThreatScore, ThreatType,
634 };
635
636 fn make_monitor() -> NetworkSecurityMonitor {
641 NetworkSecurityMonitor::new(100, 50.0)
642 }
643
644 const T0: u64 = 1_700_000_000_000; fn record(
647 mon: &mut NetworkSecurityMonitor,
648 peer: &str,
649 tt: ThreatType,
650 level: ThreatLevel,
651 ) -> u64 {
652 mon.record_event(
653 peer.to_owned(),
654 tt,
655 level,
656 "test".to_owned(),
657 vec!["evidence".to_owned()],
658 T0,
659 )
660 }
661
662 #[test]
667 fn threat_level_ordering_none_is_smallest() {
668 assert!(ThreatLevel::None < ThreatLevel::Low);
669 }
670
671 #[test]
672 fn threat_level_ordering_critical_is_largest() {
673 assert!(ThreatLevel::Critical > ThreatLevel::High);
674 }
675
676 #[test]
677 fn threat_level_ordering_full_chain() {
678 let levels = [
679 ThreatLevel::None,
680 ThreatLevel::Low,
681 ThreatLevel::Medium,
682 ThreatLevel::High,
683 ThreatLevel::Critical,
684 ];
685 for window in levels.windows(2) {
686 assert!(window[0] < window[1]);
687 }
688 }
689
690 #[test]
691 fn threat_level_eq() {
692 assert_eq!(ThreatLevel::High, ThreatLevel::High);
693 assert_ne!(ThreatLevel::Low, ThreatLevel::Medium);
694 }
695
696 #[test]
701 fn score_contribution_none_is_zero() {
702 assert_eq!(ThreatLevel::None.score_contribution(), 0.0);
703 }
704
705 #[test]
706 fn score_contribution_values() {
707 assert_eq!(ThreatLevel::Low.score_contribution(), 5.0);
708 assert_eq!(ThreatLevel::Medium.score_contribution(), 15.0);
709 assert_eq!(ThreatLevel::High.score_contribution(), 30.0);
710 assert_eq!(ThreatLevel::Critical.score_contribution(), 50.0);
711 }
712
713 #[test]
718 fn event_id_is_deterministic() {
719 let e1 = SecurityEvent::new(
720 "peer1".to_owned(),
721 ThreatType::Sybil,
722 ThreatLevel::Low,
723 "desc".to_owned(),
724 T0,
725 vec![],
726 );
727 let e2 = SecurityEvent::new(
728 "peer1".to_owned(),
729 ThreatType::Sybil,
730 ThreatLevel::Low,
731 "desc".to_owned(),
732 T0,
733 vec![],
734 );
735 assert_eq!(e1.id, e2.id);
736 }
737
738 #[test]
739 fn event_id_differs_for_different_peers() {
740 let e1 = SecurityEvent::new(
741 "peerA".to_owned(),
742 ThreatType::DDoS,
743 ThreatLevel::High,
744 "x".to_owned(),
745 T0,
746 vec![],
747 );
748 let e2 = SecurityEvent::new(
749 "peerB".to_owned(),
750 ThreatType::DDoS,
751 ThreatLevel::High,
752 "x".to_owned(),
753 T0,
754 vec![],
755 );
756 assert_ne!(e1.id, e2.id);
757 }
758
759 #[test]
760 fn event_id_differs_for_different_timestamps() {
761 let e1 = SecurityEvent::new(
762 "peer1".to_owned(),
763 ThreatType::Eclipse,
764 ThreatLevel::Medium,
765 "x".to_owned(),
766 T0,
767 vec![],
768 );
769 let e2 = SecurityEvent::new(
770 "peer1".to_owned(),
771 ThreatType::Eclipse,
772 ThreatLevel::Medium,
773 "x".to_owned(),
774 T0 + 1,
775 vec![],
776 );
777 assert_ne!(e1.id, e2.id);
778 }
779
780 #[test]
785 fn record_event_returns_nonzero_id() {
786 let mut mon = make_monitor();
787 let id = record(&mut mon, "peer1", ThreatType::Sybil, ThreatLevel::Low);
788 assert_ne!(id, 0);
789 }
790
791 #[test]
792 fn threat_score_unknown_peer_is_zero() {
793 let mut mon = make_monitor();
794 assert_eq!(mon.threat_score("ghost", T0), 0.0);
795 }
796
797 #[test]
798 fn threat_score_accumulates_after_low_event() {
799 let mut mon = make_monitor();
800 record(&mut mon, "peer1", ThreatType::Sybil, ThreatLevel::Low);
801 let score = mon.threat_score("peer1", T0);
802 assert!((score - 5.0).abs() < 1e-9);
803 }
804
805 #[test]
806 fn threat_score_accumulates_multiple_events() {
807 let mut mon = make_monitor();
808 record(&mut mon, "peer1", ThreatType::DDoS, ThreatLevel::Low); record(&mut mon, "peer1", ThreatType::DDoS, ThreatLevel::Medium); let score = mon.threat_score("peer1", T0);
811 assert!((score - 20.0).abs() < 1e-9);
812 }
813
814 #[test]
815 fn threat_score_capped_at_100() {
816 let mut mon = make_monitor();
817 for _ in 0..10 {
818 record(&mut mon, "peer1", ThreatType::DDoS, ThreatLevel::Critical); }
820 let score = mon.threat_score("peer1", T0);
821 assert!(score <= 100.0);
822 }
823
824 #[test]
829 fn decay_no_change_within_same_hour() {
830 let mut score = ThreatScore::new("p".to_owned(), T0);
831 score.add(40.0, T0);
832 NetworkSecurityMonitor::apply_decay(&mut score, T0 + 3_599_999);
833 assert!((score.score - 40.0).abs() < 1e-9);
834 }
835
836 #[test]
837 fn decay_one_hour_reduces_by_10_percent() {
838 let mut score = ThreatScore::new("p".to_owned(), T0);
839 score.add(100.0, T0);
840 NetworkSecurityMonitor::apply_decay(&mut score, T0 + 3_600_000);
841 let expected = 100.0 * 0.9;
842 assert!((score.score - expected).abs() < 1e-6);
843 }
844
845 #[test]
846 fn decay_two_hours() {
847 let mut score = ThreatScore::new("p".to_owned(), T0);
848 score.add(100.0, T0);
849 NetworkSecurityMonitor::apply_decay(&mut score, T0 + 7_200_000);
850 let expected = 100.0 * 0.9_f64.powi(2);
851 assert!((score.score - expected).abs() < 1e-6);
852 }
853
854 #[test]
855 fn decay_updates_last_updated_timestamp() {
856 let t1 = T0 + 3_600_000;
857 let mut score = ThreatScore::new("p".to_owned(), T0);
858 score.add(50.0, T0);
859 NetworkSecurityMonitor::apply_decay(&mut score, t1);
860 assert_eq!(score.last_updated, t1);
861 }
862
863 #[test]
864 fn decay_noop_when_now_equals_last_updated() {
865 let mut score = ThreatScore::new("p".to_owned(), T0);
866 score.add(60.0, T0);
867 NetworkSecurityMonitor::apply_decay(&mut score, T0);
868 assert!((score.score - 60.0).abs() < 1e-9);
869 }
870
871 #[test]
876 fn auto_incident_created_when_threshold_crossed() {
877 let mut mon = NetworkSecurityMonitor::new(100, 50.0);
878 record(
880 &mut mon,
881 "bad_peer",
882 ThreatType::Eclipse,
883 ThreatLevel::Critical,
884 );
885 let open = mon.open_incidents();
886 assert_eq!(open.len(), 1);
887 }
888
889 #[test]
890 fn no_duplicate_incident_for_same_peer() {
891 let mut mon = NetworkSecurityMonitor::new(100, 10.0);
892 record(
894 &mut mon,
895 "bad_peer",
896 ThreatType::Sybil,
897 ThreatLevel::Critical,
898 );
899 record(
900 &mut mon,
901 "bad_peer",
902 ThreatType::Sybil,
903 ThreatLevel::Critical,
904 );
905 let open = mon.open_incidents();
906 assert_eq!(open.len(), 1);
907 }
908
909 #[test]
910 fn no_incident_below_threshold() {
911 let mut mon = NetworkSecurityMonitor::new(100, 80.0);
912 record(&mut mon, "peer1", ThreatType::DDoS, ThreatLevel::Low); assert!(mon.open_incidents().is_empty());
914 }
915
916 #[test]
921 fn create_incident_manually() {
922 let mut mon = make_monitor();
923 let id = mon.create_incident(vec![1, 2, 3], T0);
924 assert_ne!(id, 0);
925 assert_eq!(mon.open_incidents().len(), 1);
926 }
927
928 #[test]
929 fn update_incident_status_to_investigating() {
930 let mut mon = make_monitor();
931 let id = mon.create_incident(vec![], T0);
932 assert!(mon.update_incident_status(id, IncidentStatus::Investigating));
933 let open = mon.open_incidents();
934 assert_eq!(open.len(), 1);
935 assert_eq!(open[0].status, IncidentStatus::Investigating);
936 }
937
938 #[test]
939 fn update_incident_status_to_resolved() {
940 let mut mon = make_monitor();
941 let id = mon.create_incident(vec![], T0);
942 mon.update_incident_status(id, IncidentStatus::Resolved);
943 assert!(mon.open_incidents().is_empty());
944 }
945
946 #[test]
947 fn update_incident_status_false_positive() {
948 let mut mon = make_monitor();
949 let id = mon.create_incident(vec![], T0);
950 mon.update_incident_status(id, IncidentStatus::FalsePositive);
951 assert!(mon.open_incidents().is_empty());
952 }
953
954 #[test]
955 fn update_incident_unknown_id_returns_false() {
956 let mut mon = make_monitor();
957 assert!(!mon.update_incident_status(9_999_999, IncidentStatus::Resolved));
958 }
959
960 #[test]
965 fn events_for_peer_returns_only_matching() {
966 let mut mon = make_monitor();
967 record(&mut mon, "peerA", ThreatType::Sybil, ThreatLevel::Low);
968 record(&mut mon, "peerB", ThreatType::DDoS, ThreatLevel::High);
969 let evs = mon.events_for_peer("peerA");
970 assert_eq!(evs.len(), 1);
971 assert_eq!(evs[0].peer_id, "peerA");
972 }
973
974 #[test]
975 fn events_for_peer_empty_for_unknown() {
976 let mon = make_monitor();
977 assert!(mon.events_for_peer("nobody").is_empty());
978 }
979
980 #[test]
981 fn events_by_threat_type_filters_correctly() {
982 let mut mon = make_monitor();
983 record(&mut mon, "p1", ThreatType::Sybil, ThreatLevel::Low);
984 record(&mut mon, "p2", ThreatType::DDoS, ThreatLevel::High);
985 record(&mut mon, "p3", ThreatType::Sybil, ThreatLevel::Medium);
986 let sybil = mon.events_by_threat_type(&ThreatType::Sybil);
987 assert_eq!(sybil.len(), 2);
988 for e in sybil {
989 assert_eq!(e.threat_type, ThreatType::Sybil);
990 }
991 }
992
993 #[test]
994 fn events_since_filters_by_timestamp() {
995 let mut mon = make_monitor();
996 mon.record_event(
997 "p1".to_owned(),
998 ThreatType::DDoS,
999 ThreatLevel::Low,
1000 "x".to_owned(),
1001 vec![],
1002 T0,
1003 );
1004 mon.record_event(
1005 "p2".to_owned(),
1006 ThreatType::DDoS,
1007 ThreatLevel::Low,
1008 "y".to_owned(),
1009 vec![],
1010 T0 + 5000,
1011 );
1012 let recent = mon.events_since(T0 + 1);
1013 assert_eq!(recent.len(), 1);
1014 assert_eq!(recent[0].peer_id, "p2");
1015 }
1016
1017 #[test]
1018 fn events_since_includes_equal_timestamp() {
1019 let mut mon = make_monitor();
1020 mon.record_event(
1021 "p1".to_owned(),
1022 ThreatType::Sybil,
1023 ThreatLevel::Low,
1024 "x".to_owned(),
1025 vec![],
1026 T0,
1027 );
1028 let evs = mon.events_since(T0);
1029 assert_eq!(evs.len(), 1);
1030 }
1031
1032 #[test]
1037 fn top_threats_returns_highest_scores_first() {
1038 let mut mon = make_monitor();
1039 record(&mut mon, "low", ThreatType::Sybil, ThreatLevel::Low); record(&mut mon, "high", ThreatType::DDoS, ThreatLevel::High); record(&mut mon, "med", ThreatType::Eclipse, ThreatLevel::Medium); let top = mon.top_threats(3, T0);
1043 assert_eq!(top[0].peer_id, "high");
1044 assert_eq!(top[1].peer_id, "med");
1045 assert_eq!(top[2].peer_id, "low");
1046 }
1047
1048 #[test]
1049 fn top_threats_n_larger_than_peers() {
1050 let mut mon = make_monitor();
1051 record(&mut mon, "p1", ThreatType::Sybil, ThreatLevel::Low);
1052 let top = mon.top_threats(10, T0);
1053 assert_eq!(top.len(), 1);
1054 }
1055
1056 #[test]
1061 fn clear_peer_history_removes_events_and_score() {
1062 let mut mon = make_monitor();
1063 record(&mut mon, "peer1", ThreatType::Sybil, ThreatLevel::High);
1064 record(&mut mon, "peer1", ThreatType::DDoS, ThreatLevel::Medium);
1065 record(&mut mon, "peer2", ThreatType::Eclipse, ThreatLevel::Low);
1066 let removed = mon.clear_peer_history("peer1");
1067 assert_eq!(removed, 2);
1068 assert_eq!(mon.threat_score("peer1", T0), 0.0);
1069 assert_eq!(mon.events_for_peer("peer1").len(), 0);
1070 assert_eq!(mon.events_for_peer("peer2").len(), 1);
1071 }
1072
1073 #[test]
1074 fn clear_peer_history_unknown_peer_returns_zero() {
1075 let mut mon = make_monitor();
1076 assert_eq!(mon.clear_peer_history("nobody"), 0);
1077 }
1078
1079 #[test]
1084 fn rolling_buffer_evicts_oldest_when_full() {
1085 let mut mon = NetworkSecurityMonitor::new(3, 200.0); let id1 = record(&mut mon, "p", ThreatType::Sybil, ThreatLevel::None);
1087 let _id2 = record(&mut mon, "p", ThreatType::DDoS, ThreatLevel::None);
1088 let _id3 = record(&mut mon, "p", ThreatType::Eclipse, ThreatLevel::None);
1089 let _id4 = record(&mut mon, "p", ThreatType::ReplayAttack, ThreatLevel::None);
1090 let present: Vec<u64> = mon.events_for_peer("p").iter().map(|e| e.id).collect();
1092 assert!(!present.contains(&id1));
1093 assert_eq!(present.len(), 3);
1094 }
1095
1096 #[test]
1101 fn stats_empty_monitor() {
1102 let mut mon = make_monitor();
1103 let s = mon.stats(T0);
1104 assert_eq!(s.total_events, 0);
1105 assert_eq!(s.open_incidents, 0);
1106 assert_eq!(s.resolved_incidents, 0);
1107 assert_eq!(s.high_threat_peers, 0);
1108 assert_eq!(s.avg_threat_score, 0.0);
1109 }
1110
1111 #[test]
1112 fn stats_counts_open_incidents() {
1113 let mut mon = make_monitor();
1114 mon.create_incident(vec![], T0);
1115 mon.create_incident(vec![], T0);
1116 let s = mon.stats(T0);
1117 assert_eq!(s.open_incidents, 2);
1118 }
1119
1120 #[test]
1121 fn stats_counts_resolved_incidents() {
1122 let mut mon = make_monitor();
1123 let id = mon.create_incident(vec![], T0);
1124 mon.update_incident_status(id, IncidentStatus::Resolved);
1125 let s = mon.stats(T0);
1126 assert_eq!(s.resolved_incidents, 1);
1127 assert_eq!(s.open_incidents, 0);
1128 }
1129
1130 #[test]
1131 fn stats_high_threat_peers_threshold_50() {
1132 let mut mon = NetworkSecurityMonitor::new(100, 200.0); record(&mut mon, "clean", ThreatType::Sybil, ThreatLevel::Low); record(&mut mon, "dirty", ThreatType::DDoS, ThreatLevel::Critical); let s = mon.stats(T0);
1136 assert_eq!(s.high_threat_peers, 1);
1137 }
1138
1139 #[test]
1140 fn stats_avg_threat_score() {
1141 let mut mon = NetworkSecurityMonitor::new(100, 200.0);
1142 record(&mut mon, "p1", ThreatType::Sybil, ThreatLevel::Low); record(&mut mon, "p2", ThreatType::DDoS, ThreatLevel::Medium); let s = mon.stats(T0);
1145 let expected_avg = (5.0 + 15.0) / 2.0;
1146 assert!((s.avg_threat_score - expected_avg).abs() < 1e-6);
1147 }
1148
1149 #[test]
1154 fn threat_type_names_are_correct() {
1155 assert_eq!(ThreatType::Sybil.name(), "Sybil");
1156 assert_eq!(ThreatType::Eclipse.name(), "Eclipse");
1157 assert_eq!(ThreatType::DDoS.name(), "DDoS");
1158 assert_eq!(ThreatType::ManInTheMiddle.name(), "ManInTheMiddle");
1159 assert_eq!(ThreatType::ReplayAttack.name(), "ReplayAttack");
1160 assert_eq!(ThreatType::RoutingPoison.name(), "RoutingPoison");
1161 assert_eq!(ThreatType::DataTampering.name(), "DataTampering");
1162 }
1163
1164 #[test]
1169 fn default_monitor_threshold_is_50() {
1170 let mut mon = NetworkSecurityMonitor::default_monitor();
1171 record(&mut mon, "p", ThreatType::Sybil, ThreatLevel::Critical);
1173 assert_eq!(mon.open_incidents().len(), 1);
1175 }
1176
1177 #[test]
1178 fn zero_threshold_replaced_with_50() {
1179 let mon = NetworkSecurityMonitor::new(10, 0.0);
1180 let _ = format!("{:?}", mon.events_for_peer("x"));
1183 }
1184
1185 #[test]
1190 fn incident_contains_peer_event_ids() {
1191 let mut mon = NetworkSecurityMonitor::new(100, 10.0);
1192 let eid = record(&mut mon, "p", ThreatType::DDoS, ThreatLevel::Critical);
1193 let incidents = mon.open_incidents();
1194 assert_eq!(incidents.len(), 1);
1195 assert!(incidents[0].events.contains(&eid));
1196 }
1197
1198 #[test]
1203 fn different_peers_have_independent_scores() {
1204 let mut mon = make_monitor();
1205 record(&mut mon, "alice", ThreatType::Sybil, ThreatLevel::High); record(&mut mon, "bob", ThreatType::DDoS, ThreatLevel::Medium); assert!((mon.threat_score("alice", T0) - 30.0).abs() < 1e-9);
1208 assert!((mon.threat_score("bob", T0) - 15.0).abs() < 1e-9);
1209 }
1210}