1use memf_correlate::event::{Entity, Finding, ForensicEvent, Severity};
4use memf_correlate::mitre::MitreAttackId;
5use memf_correlate::traits::IntoForensicEvents;
6
7use crate::types::{
8 AuditTamperInfo, ConnectionInfo, ContainerEscapeCorrelateInfo, CpuPinningInfo, FdAbuseInfo,
9 FdAbuseType, FuseAbuseInfo, HiddenProcessInfo, ModuleInfo, ModuleState, ProcessInfo,
10 ProcessState, SharedMemAnomalyInfo, UserNsEscalationInfo, VdsoTamperInfo, VmaInfo,
11};
12
13impl IntoForensicEvents for VmaInfo {
14 fn into_forensic_events(self) -> Vec<ForensicEvent> {
15 let (severity, finding, mitre, confidence) =
16 if self.flags.exec && self.flags.write && !self.file_backed {
17 (
19 Severity::High,
20 Finding::ProcessHollowing,
21 MitreAttackId::vec("T1055"),
22 0.9f64,
23 )
24 } else if self.flags.exec && !self.file_backed {
25 (
27 Severity::Medium,
28 Finding::DefenseEvasion,
29 MitreAttackId::vec("T1055"),
30 0.6f64,
31 )
32 } else {
33 (
34 Severity::Info,
35 Finding::Other("vma_enumerated".into()),
36 vec![],
37 0.3f64,
38 )
39 };
40
41 vec![ForensicEvent::builder()
42 .source_walker("linux_vma")
43 .entity(Entity::Process {
44 pid: self.pid as u32,
45 name: self.comm.clone(),
46 ppid: None,
47 })
48 .finding(finding)
49 .severity(severity)
50 .confidence(confidence)
51 .mitre_attack(mitre)
52 .build()]
53 }
54}
55
56impl IntoForensicEvents for ProcessInfo {
57 fn into_forensic_events(self) -> Vec<ForensicEvent> {
58 let (severity, finding, mitre, confidence) = if self.comm.is_empty() {
59 (
61 Severity::High,
62 Finding::DefenseEvasion,
63 MitreAttackId::vec("T1564"),
64 0.9f64,
65 )
66 } else if self.state == ProcessState::Zombie {
67 (
69 Severity::Medium,
70 Finding::DefenseEvasion,
71 MitreAttackId::vec("T1564"),
72 0.7f64,
73 )
74 } else if self.cr3.is_none() && self.ppid != 0 {
75 (
77 Severity::Medium,
78 Finding::Other("suspicious_kthread".into()),
79 vec![],
80 0.6f64,
81 )
82 } else {
83 (
84 Severity::Info,
85 Finding::Other("process_enumerated".into()),
86 vec![],
87 0.4f64,
88 )
89 };
90
91 vec![ForensicEvent::builder()
92 .source_walker("linux_process")
93 .entity(Entity::Process {
94 pid: self.pid as u32,
95 name: self.comm.clone(),
96 ppid: Some(self.ppid as u32),
97 })
98 .finding(finding)
99 .severity(severity)
100 .confidence(confidence)
101 .mitre_attack(mitre)
102 .build()]
103 }
104}
105
106impl IntoForensicEvents for ConnectionInfo {
107 fn into_forensic_events(self) -> Vec<ForensicEvent> {
108 let is_loopback = self.remote_addr == "127.0.0.1"
109 || self.remote_addr == "::1"
110 || self.remote_addr.is_empty();
111
112 let (severity, finding, mitre, confidence) =
113 if matches!(self.remote_port, 4444 | 1337 | 31337) {
114 (
116 Severity::High,
117 Finding::NetworkBeaconing,
118 MitreAttackId::vec("T1071"),
119 0.8f64,
120 )
121 } else if self.pid.is_none() {
122 (
124 Severity::High,
125 Finding::DefenseEvasion,
126 MitreAttackId::vec("T1095"),
127 0.85f64,
128 )
129 } else if self.remote_port == 0 && !is_loopback {
130 (
132 Severity::Medium,
133 Finding::NetworkBeaconing,
134 MitreAttackId::vec("T1071"),
135 0.6f64,
136 )
137 } else {
138 (
139 Severity::Info,
140 Finding::Other("connection_enumerated".into()),
141 vec![],
142 0.4f64,
143 )
144 };
145
146 let src = format!("{}:{}", self.local_addr, self.local_port)
147 .parse()
148 .unwrap_or_else(|_| std::net::SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0)));
149 let dst = format!("{}:{}", self.remote_addr, self.remote_port)
150 .parse()
151 .unwrap_or_else(|_| std::net::SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0)));
152
153 vec![ForensicEvent::builder()
154 .source_walker("linux_connection")
155 .entity(Entity::Connection {
156 src,
157 dst,
158 proto: memf_correlate::event::Protocol::Tcp,
159 })
160 .finding(finding)
161 .severity(severity)
162 .confidence(confidence)
163 .mitre_attack(mitre)
164 .build()]
165 }
166}
167
168impl IntoForensicEvents for ModuleInfo {
169 fn into_forensic_events(self) -> Vec<ForensicEvent> {
170 let (severity, finding, mitre, confidence) = if self.name.is_empty() {
171 (
173 Severity::High,
174 Finding::DefenseEvasion,
175 MitreAttackId::vec("T1014"),
176 0.9f64,
177 )
178 } else if matches!(self.state, ModuleState::Going) {
179 (
181 Severity::Medium,
182 Finding::DefenseEvasion,
183 MitreAttackId::vec("T1014"),
184 0.6f64,
185 )
186 } else {
187 (
188 Severity::Info,
189 Finding::Other("module_enumerated".into()),
190 vec![],
191 0.3f64,
192 )
193 };
194
195 vec![ForensicEvent::builder()
196 .source_walker("linux_module")
197 .entity(Entity::Module {
198 name: self.name.clone(),
199 base: self.base_addr,
200 size: self.size,
201 })
202 .finding(finding)
203 .severity(severity)
204 .confidence(confidence)
205 .mitre_attack(mitre)
206 .build()]
207 }
208}
209
210impl IntoForensicEvents for HiddenProcessInfo {
215 fn into_forensic_events(self) -> Vec<ForensicEvent> {
216 let (severity, finding, mitre, confidence) =
217 if self.present_in_pid_ns && !self.present_in_task_list {
218 (
220 Severity::Critical,
221 Finding::DefenseEvasion,
222 MitreAttackId::vec("T1014"),
223 0.95f64,
224 )
225 } else if self.present_in_pid_hash && !self.present_in_task_list {
226 (
228 Severity::High,
229 Finding::DefenseEvasion,
230 MitreAttackId::vec("T1014"),
231 0.85f64,
232 )
233 } else if self.present_in_task_list && !self.present_in_pid_ns {
234 (
236 Severity::High,
237 Finding::DefenseEvasion,
238 MitreAttackId::vec("T1014"),
239 0.8f64,
240 )
241 } else {
242 (
243 Severity::Info,
244 Finding::Other("process_enumerated".into()),
245 vec![],
246 0.3f64,
247 )
248 };
249
250 vec![ForensicEvent::builder()
251 .source_walker("linux_proc_hidden")
252 .entity(Entity::Process {
253 pid: self.pid as u32,
254 name: self.comm.clone(),
255 ppid: None,
256 })
257 .finding(finding)
258 .severity(severity)
259 .confidence(confidence)
260 .mitre_attack(mitre)
261 .build()]
262 }
263}
264
265impl IntoForensicEvents for VdsoTamperInfo {
266 fn into_forensic_events(self) -> Vec<ForensicEvent> {
267 let (severity, finding, mitre, confidence) =
268 if self.differs_from_canonical && self.diff_byte_count > 16 {
269 (
271 Severity::Critical,
272 Finding::ProcessHollowing,
273 MitreAttackId::vec("T1055"),
274 0.95f64,
275 )
276 } else if self.differs_from_canonical {
277 (
279 Severity::High,
280 Finding::DefenseEvasion,
281 MitreAttackId::vec("T1055"),
282 0.8f64,
283 )
284 } else {
285 (
286 Severity::Info,
287 Finding::Other("vdso_clean".into()),
288 vec![],
289 0.3f64,
290 )
291 };
292
293 vec![ForensicEvent::builder()
294 .source_walker("linux_vdso_tamper")
295 .entity(Entity::Process {
296 pid: self.pid as u32,
297 name: self.comm.clone(),
298 ppid: None,
299 })
300 .finding(finding)
301 .severity(severity)
302 .confidence(confidence)
303 .mitre_attack(mitre)
304 .build()]
305 }
306}
307
308impl IntoForensicEvents for UserNsEscalationInfo {
309 fn into_forensic_events(self) -> Vec<ForensicEvent> {
310 let (severity, finding, mitre, confidence) =
311 if self.has_cap_sys_admin && self.owner_uid != self.process_uid {
312 (
314 Severity::Critical,
315 Finding::DefenseEvasion,
316 MitreAttackId::vec("T1611"),
317 0.9f64,
318 )
319 } else if self.ns_depth > 3 {
320 (
322 Severity::High,
323 Finding::DefenseEvasion,
324 MitreAttackId::vec("T1611"),
325 0.7f64,
326 )
327 } else if self.has_cap_sys_admin && self.owner_uid == 0 {
328 (
330 Severity::Medium,
331 Finding::DefenseEvasion,
332 MitreAttackId::vec("T1548"),
333 0.6f64,
334 )
335 } else {
336 (
337 Severity::Info,
338 Finding::Other("user_ns_enumerated".into()),
339 vec![],
340 0.3f64,
341 )
342 };
343
344 vec![ForensicEvent::builder()
345 .source_walker("linux_user_ns")
346 .entity(Entity::Process {
347 pid: self.pid as u32,
348 name: self.comm.clone(),
349 ppid: None,
350 })
351 .finding(finding)
352 .severity(severity)
353 .confidence(confidence)
354 .mitre_attack(mitre)
355 .build()]
356 }
357}
358
359impl IntoForensicEvents for AuditTamperInfo {
360 fn into_forensic_events(self) -> Vec<ForensicEvent> {
361 let (severity, finding, mitre, confidence) = if self.audit_globally_disabled {
362 (
364 Severity::Critical,
365 Finding::DefenseEvasion,
366 MitreAttackId::vec("T1562"),
367 0.95f64,
368 )
369 } else if !self.suppressed_pids.is_empty() {
370 (
372 Severity::High,
373 Finding::DefenseEvasion,
374 MitreAttackId::vec("T1562"),
375 0.85f64,
376 )
377 } else if self.backlog_limit < 64 {
378 (
380 Severity::Medium,
381 Finding::DefenseEvasion,
382 MitreAttackId::vec("T1562"),
383 0.6f64,
384 )
385 } else {
386 (
387 Severity::Info,
388 Finding::Other("audit_enumerated".into()),
389 vec![],
390 0.3f64,
391 )
392 };
393
394 vec![ForensicEvent::builder()
395 .source_walker("linux_netlink_audit")
396 .entity(Entity::File {
397 path: "kernel:audit".into(),
398 })
399 .finding(finding)
400 .severity(severity)
401 .confidence(confidence)
402 .mitre_attack(mitre)
403 .build()]
404 }
405}
406
407impl IntoForensicEvents for CpuPinningInfo {
408 fn into_forensic_events(self) -> Vec<ForensicEvent> {
409 let (severity, finding, mitre, confidence) =
410 if self.pinned_cpu_count == 1 && self.cpu_time_ns > 1_000_000_000 {
411 (
413 Severity::High,
414 Finding::Other("cryptomining_suspected".into()),
415 MitreAttackId::vec("T1496"),
416 0.8f64,
417 )
418 } else if self.sched_policy == 3 || self.sched_policy == 5 {
419 (
421 Severity::Medium,
422 Finding::Other("stealth_scheduling".into()),
423 MitreAttackId::vec("T1496"),
424 0.5f64,
425 )
426 } else {
427 (
428 Severity::Info,
429 Finding::Other("cpu_pinning_enumerated".into()),
430 vec![],
431 0.3f64,
432 )
433 };
434
435 vec![ForensicEvent::builder()
436 .source_walker("linux_cpu_pinning")
437 .entity(Entity::Process {
438 pid: self.pid as u32,
439 name: self.comm.clone(),
440 ppid: None,
441 })
442 .finding(finding)
443 .severity(severity)
444 .confidence(confidence)
445 .mitre_attack(mitre)
446 .build()]
447 }
448}
449
450impl IntoForensicEvents for ContainerEscapeCorrelateInfo {
455 fn into_forensic_events(self) -> Vec<ForensicEvent> {
456 let (severity, finding, mitre, confidence) =
457 if self.has_host_mounts && self.in_non_init_pid_ns {
458 (
460 Severity::Critical,
461 Finding::DefenseEvasion,
462 MitreAttackId::vec("T1611"),
463 0.9f64,
464 )
465 } else if self.cap_sys_admin && self.in_non_init_pid_ns {
466 (
468 Severity::High,
469 Finding::DefenseEvasion,
470 MitreAttackId::vec("T1611"),
471 0.8f64,
472 )
473 } else if self.pid_ns_differs_from_cgroup_ns {
474 (
476 Severity::High,
477 Finding::DefenseEvasion,
478 MitreAttackId::vec("T1611"),
479 0.75f64,
480 )
481 } else {
482 (
483 Severity::Info,
484 Finding::Other("container_enumerated".into()),
485 vec![],
486 0.3f64,
487 )
488 };
489
490 vec![ForensicEvent::builder()
491 .source_walker("linux_container_escape")
492 .entity(Entity::Process {
493 pid: self.pid as u32,
494 name: self.comm.clone(),
495 ppid: None,
496 })
497 .finding(finding)
498 .severity(severity)
499 .confidence(confidence)
500 .mitre_attack(mitre)
501 .build()]
502 }
503}
504
505impl IntoForensicEvents for FdAbuseInfo {
506 fn into_forensic_events(self) -> Vec<ForensicEvent> {
507 let (severity, finding, mitre, confidence) =
508 if self.fd_type == FdAbuseType::SignalFd && self.signal_mask & (1u64 << 15) != 0 {
509 (
511 Severity::High,
512 Finding::DefenseEvasion,
513 MitreAttackId::vec("T1205"),
514 0.8f64,
515 )
516 } else if self.fd_type == FdAbuseType::TimerFd && self.interval_ns < 1_000_000_000 {
517 (
519 Severity::Medium,
520 Finding::NetworkBeaconing,
521 MitreAttackId::vec("T1071"),
522 0.5f64,
523 )
524 } else if self.is_cross_process_shared {
525 (
527 Severity::Medium,
528 Finding::DefenseEvasion,
529 MitreAttackId::vec("T1071"),
530 0.6f64,
531 )
532 } else {
533 (
534 Severity::Info,
535 Finding::Other("fd_enumerated".into()),
536 vec![],
537 0.3f64,
538 )
539 };
540
541 vec![ForensicEvent::builder()
542 .source_walker("linux_timerfd_signalfd")
543 .entity(Entity::Process {
544 pid: self.pid as u32,
545 name: self.comm.clone(),
546 ppid: None,
547 })
548 .finding(finding)
549 .severity(severity)
550 .confidence(confidence)
551 .mitre_attack(mitre)
552 .build()]
553 }
554}
555
556impl IntoForensicEvents for SharedMemAnomalyInfo {
557 fn into_forensic_events(self) -> Vec<ForensicEvent> {
558 let (severity, finding, mitre, confidence) = if self.is_memfd && self.is_executable {
559 (
561 Severity::Critical,
562 Finding::ProcessHollowing,
563 MitreAttackId::vec("T1027"),
564 0.9f64,
565 )
566 } else if self.has_elf_header && self.is_executable {
567 (
569 Severity::High,
570 Finding::ProcessHollowing,
571 MitreAttackId::vec("T1055"),
572 0.85f64,
573 )
574 } else if self.is_cross_uid {
575 (
577 Severity::Medium,
578 Finding::DefenseEvasion,
579 MitreAttackId::vec("T1055"),
580 0.6f64,
581 )
582 } else {
583 (
584 Severity::Info,
585 Finding::Other("shared_mem_enumerated".into()),
586 vec![],
587 0.3f64,
588 )
589 };
590
591 vec![ForensicEvent::builder()
592 .source_walker("linux_shared_mem")
593 .entity(Entity::Process {
594 pid: self.pid as u32,
595 name: self.comm.clone(),
596 ppid: None,
597 })
598 .finding(finding)
599 .severity(severity)
600 .confidence(confidence)
601 .mitre_attack(mitre)
602 .build()]
603 }
604}
605
606impl IntoForensicEvents for FuseAbuseInfo {
607 fn into_forensic_events(self) -> Vec<ForensicEvent> {
608 let (severity, finding, mitre, confidence) = if self.is_over_sensitive_path {
609 (
611 Severity::High,
612 Finding::DefenseEvasion,
613 MitreAttackId::vec("T1564"),
614 0.9f64,
615 )
616 } else if self.daemon_is_root && self.allow_other {
617 (
619 Severity::Medium,
620 Finding::DefenseEvasion,
621 MitreAttackId::vec("T1564"),
622 0.6f64,
623 )
624 } else {
625 (
626 Severity::Info,
627 Finding::Other("fuse_mount_enumerated".into()),
628 vec![],
629 0.3f64,
630 )
631 };
632
633 vec![ForensicEvent::builder()
634 .source_walker("linux_fuse")
635 .entity(Entity::Process {
636 pid: self.pid as u32,
637 name: self.comm.clone(),
638 ppid: None,
639 })
640 .finding(finding)
641 .severity(severity)
642 .confidence(confidence)
643 .mitre_attack(mitre)
644 .build()]
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::types::{ConnectionState, Protocol as LinuxProtocol, VmaFlags};
652
653 fn make_vma(pid: u64, comm: &str, exec: bool, write: bool, file_backed: bool) -> VmaInfo {
654 VmaInfo {
655 pid,
656 comm: comm.to_string(),
657 start: 0x7f00_0000_0000,
658 end: 0x7f00_0001_0000,
659 flags: VmaFlags {
660 read: true,
661 write,
662 exec,
663 shared: false,
664 },
665 pgoff: 0,
666 file_backed,
667 }
668 }
669
670 #[test]
671 fn rwx_anonymous_vma_produces_high_severity_malfind() {
672 let vma = make_vma(1234, "bash", true, true, false);
673 let events = vma.into_forensic_events();
674 assert_eq!(events.len(), 1);
675 assert_eq!(events[0].severity, Severity::High);
676 assert!(matches!(
677 events[0].finding,
678 Finding::ProcessHollowing | Finding::DefenseEvasion
679 ));
680 assert!(!events[0].mitre_attack.is_empty());
681 }
682
683 #[test]
684 fn executable_anonymous_vma_produces_medium_event() {
685 let vma = make_vma(1234, "python3", true, false, false);
687 let events = vma.into_forensic_events();
688 assert_eq!(events.len(), 1);
689 assert!(events[0].severity >= Severity::Medium);
690 }
691
692 #[test]
693 fn read_only_file_backed_vma_is_info() {
694 let vma = make_vma(1234, "cat", false, false, true);
695 let events = vma.into_forensic_events();
696 assert_eq!(events[0].severity, Severity::Info);
697 }
698
699 #[test]
700 fn rwx_vma_mitre_id_is_process_injection() {
701 let vma = make_vma(999, "evil", true, true, false);
702 let events = vma.into_forensic_events();
703 let ids: Vec<&str> = events[0]
704 .mitre_attack
705 .iter()
706 .map(memf_correlate::mitre::MitreAttackId::as_str)
707 .collect();
708 assert!(
709 ids.contains(&"T1055"),
710 "expected T1055 (Process Injection), got {ids:?}"
711 );
712 }
713
714 #[test]
715 fn entity_contains_pid_and_comm() {
716 let vma = make_vma(42, "sh", true, true, false);
717 let events = vma.into_forensic_events();
718 match &events[0].entity {
719 Entity::Process { pid, name, .. } => {
720 assert_eq!(*pid, 42u32);
721 assert_eq!(name, "sh");
722 }
723 other => panic!("expected Process entity, got {other:?}"),
724 }
725 }
726
727 #[test]
728 fn source_walker_is_linux_vma() {
729 let vma = make_vma(1, "init", false, false, true);
730 let events = vma.into_forensic_events();
731 assert_eq!(events[0].source_walker, "linux_vma");
732 }
733
734 #[test]
735 fn rwx_anonymous_vma_is_suspicious() {
736 let vma = make_vma(1234, "bash", true, true, false);
737 let events = vma.into_forensic_events();
738 assert!(events[0].is_suspicious());
739 }
740
741 #[test]
742 fn info_vma_is_not_suspicious() {
743 let vma = make_vma(1234, "cat", false, false, true);
744 let events = vma.into_forensic_events();
745 assert!(!events[0].is_suspicious());
746 }
747
748 fn make_process(
753 pid: u64,
754 ppid: u64,
755 comm: &str,
756 state: ProcessState,
757 cr3: Option<u64>,
758 ) -> ProcessInfo {
759 ProcessInfo {
760 pid,
761 ppid,
762 comm: comm.to_string(),
763 state,
764 vaddr: 0xffff_8880_0000_0000,
765 cr3,
766 start_time: 12_345_678,
767 }
768 }
769
770 #[test]
771 fn zombie_process_is_medium_defense_evasion() {
772 let p = make_process(1234, 1, "defunct", ProcessState::Zombie, Some(0x1000));
773 let events = p.into_forensic_events();
774 assert_eq!(events.len(), 1);
775 assert_eq!(events[0].severity, Severity::Medium);
776 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
777 let ids: Vec<&str> = events[0]
778 .mitre_attack
779 .iter()
780 .map(memf_correlate::mitre::MitreAttackId::as_str)
781 .collect();
782 assert!(ids.contains(&"T1564"), "expected T1564, got {ids:?}");
783 assert!((events[0].confidence - 0.7).abs() < 1e-9);
784 }
785
786 #[test]
787 fn empty_comm_is_high_severity() {
788 let p = make_process(999, 1, "", ProcessState::Running, Some(0x2000));
789 let events = p.into_forensic_events();
790 assert_eq!(events.len(), 1);
791 assert_eq!(events[0].severity, Severity::High);
792 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
793 let ids: Vec<&str> = events[0]
794 .mitre_attack
795 .iter()
796 .map(memf_correlate::mitre::MitreAttackId::as_str)
797 .collect();
798 assert!(ids.contains(&"T1564"), "expected T1564, got {ids:?}");
799 assert!((events[0].confidence - 0.9).abs() < 1e-9);
800 }
801
802 #[test]
803 fn normal_process_is_info() {
804 let p = make_process(42, 1, "bash", ProcessState::Running, Some(0x3000));
805 let events = p.into_forensic_events();
806 assert_eq!(events.len(), 1);
807 assert_eq!(events[0].severity, Severity::Info);
808 }
809
810 #[test]
811 fn process_entity_has_correct_pid_ppid_comm() {
812 let p = make_process(77, 5, "sshd", ProcessState::Sleeping, Some(0x4000));
813 let events = p.into_forensic_events();
814 assert_eq!(events[0].source_walker, "linux_process");
815 match &events[0].entity {
816 Entity::Process { pid, name, ppid } => {
817 assert_eq!(*pid, 77u32);
818 assert_eq!(name, "sshd");
819 assert_eq!(*ppid, Some(5u32));
820 }
821 other => panic!("expected Process entity, got {other:?}"),
822 }
823 }
824
825 #[test]
826 fn info_process_is_not_suspicious() {
827 let p = make_process(100, 1, "nginx", ProcessState::Sleeping, Some(0x5000));
828 let events = p.into_forensic_events();
829 assert!(!events[0].is_suspicious());
830 }
831
832 fn make_conn(remote_addr: &str, remote_port: u16, pid: Option<u64>) -> ConnectionInfo {
837 ConnectionInfo {
838 protocol: LinuxProtocol::Tcp,
839 local_addr: "192.168.1.10".to_string(),
840 local_port: 54321,
841 remote_addr: remote_addr.to_string(),
842 remote_port,
843 state: ConnectionState::Established,
844 pid,
845 }
846 }
847
848 #[test]
849 fn c2_port_connection_is_high_beaconing() {
850 for port in [4444u16, 1337, 31337] {
851 let c = make_conn("10.0.0.1", port, Some(100));
852 let events = c.into_forensic_events();
853 assert_eq!(events.len(), 1, "port {port}");
854 assert_eq!(events[0].severity, Severity::High, "port {port}");
855 assert!(
856 matches!(events[0].finding, Finding::NetworkBeaconing),
857 "port {port}"
858 );
859 let ids: Vec<&str> = events[0]
860 .mitre_attack
861 .iter()
862 .map(memf_correlate::mitre::MitreAttackId::as_str)
863 .collect();
864 assert!(
865 ids.contains(&"T1071"),
866 "expected T1071 for port {port}, got {ids:?}"
867 );
868 assert!((events[0].confidence - 0.8).abs() < 1e-9, "port {port}");
869 }
870 }
871
872 #[test]
873 fn no_owning_pid_is_high_defense_evasion() {
874 let c = make_conn("8.8.8.8", 443, None);
875 let events = c.into_forensic_events();
876 assert_eq!(events.len(), 1);
877 assert_eq!(events[0].severity, Severity::High);
878 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
879 let ids: Vec<&str> = events[0]
880 .mitre_attack
881 .iter()
882 .map(memf_correlate::mitre::MitreAttackId::as_str)
883 .collect();
884 assert!(ids.contains(&"T1095"), "expected T1095, got {ids:?}");
885 assert!((events[0].confidence - 0.85).abs() < 1e-9);
886 }
887
888 #[test]
889 fn normal_connection_is_info() {
890 let c = make_conn("93.184.216.34", 443, Some(200));
891 let events = c.into_forensic_events();
892 assert_eq!(events.len(), 1);
893 assert_eq!(events[0].severity, Severity::Info);
894 assert!(matches!(events[0].finding, Finding::Other(_)));
895 }
896
897 #[test]
898 fn connection_entity_has_correct_src_dst() {
899 let c = make_conn("10.0.0.1", 4444, Some(42));
900 let events = c.into_forensic_events();
901 assert_eq!(events[0].source_walker, "linux_connection");
902 match &events[0].entity {
903 Entity::Connection { src, dst, .. } => {
904 assert_eq!(src.port(), 54321);
905 assert_eq!(dst.port(), 4444);
906 assert_eq!(dst.ip().to_string(), "10.0.0.1");
907 }
908 other => panic!("expected Connection entity, got {other:?}"),
909 }
910 }
911
912 #[test]
913 fn hidden_connection_is_suspicious() {
914 let c = make_conn("8.8.8.8", 443, None);
915 let events = c.into_forensic_events();
916 assert!(events[0].is_suspicious());
917 }
918
919 fn make_module(name: &str, state: ModuleState) -> ModuleInfo {
924 ModuleInfo {
925 name: name.to_string(),
926 base_addr: 0xffff_c000_0000_0000,
927 size: 0x4000,
928 state,
929 }
930 }
931
932 #[test]
933 fn live_named_module_is_info() {
934 let m = make_module("ext4", ModuleState::Live);
935 let events = m.into_forensic_events();
936 assert_eq!(events.len(), 1);
937 assert_eq!(events[0].severity, Severity::Info);
938 }
939
940 #[test]
941 fn going_state_module_is_medium_defense_evasion() {
942 let m = make_module("rootkit", ModuleState::Going);
944 let events = m.into_forensic_events();
945 assert_eq!(events[0].severity, Severity::Medium);
946 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
947 let ids: Vec<&str> = events[0]
948 .mitre_attack
949 .iter()
950 .map(memf_correlate::mitre::MitreAttackId::as_str)
951 .collect();
952 assert!(ids.contains(&"T1014"), "expected T1014");
953 }
954
955 #[test]
956 fn empty_name_module_is_high() {
957 let m = make_module("", ModuleState::Live);
959 let events = m.into_forensic_events();
960 assert_eq!(events[0].severity, Severity::High);
961 let ids: Vec<&str> = events[0]
962 .mitre_attack
963 .iter()
964 .map(memf_correlate::mitre::MitreAttackId::as_str)
965 .collect();
966 assert!(ids.contains(&"T1014"), "expected T1014");
967 }
968
969 #[test]
970 fn module_source_walker_is_linux_module() {
971 let m = make_module("xfs", ModuleState::Live);
972 let events = m.into_forensic_events();
973 assert_eq!(events[0].source_walker, "linux_module");
974 }
975
976 fn make_hidden_process(
981 pid: u64,
982 comm: &str,
983 present_in_pid_ns: bool,
984 present_in_task_list: bool,
985 present_in_pid_hash: bool,
986 ) -> HiddenProcessInfo {
987 HiddenProcessInfo {
988 pid,
989 comm: comm.to_string(),
990 present_in_pid_ns,
991 present_in_task_list,
992 present_in_pid_hash,
993 }
994 }
995
996 #[test]
997 fn pid_ns_only_process_is_critical_rootkit() {
998 let h = make_hidden_process(1234, "rootkit", true, false, false);
999 let events = h.into_forensic_events();
1000 assert_eq!(events.len(), 1);
1001 assert_eq!(events[0].severity, Severity::Critical);
1002 let ids: Vec<&str> = events[0]
1003 .mitre_attack
1004 .iter()
1005 .map(memf_correlate::mitre::MitreAttackId::as_str)
1006 .collect();
1007 assert!(ids.contains(&"T1014"), "expected T1014, got {ids:?}");
1008 assert!((events[0].confidence - 0.95).abs() < 1e-9);
1009 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
1010 }
1011
1012 #[test]
1013 fn task_list_only_process_is_high() {
1014 let h = make_hidden_process(999, "ghost", false, true, false);
1015 let events = h.into_forensic_events();
1016 assert_eq!(events.len(), 1);
1017 assert_eq!(events[0].severity, Severity::High);
1018 let ids: Vec<&str> = events[0]
1019 .mitre_attack
1020 .iter()
1021 .map(memf_correlate::mitre::MitreAttackId::as_str)
1022 .collect();
1023 assert!(ids.contains(&"T1014"), "expected T1014, got {ids:?}");
1024 assert!((events[0].confidence - 0.8).abs() < 1e-9);
1025 }
1026
1027 #[test]
1028 fn pid_hash_without_task_list_is_high() {
1029 let h = make_hidden_process(555, "hidden", false, false, true);
1030 let events = h.into_forensic_events();
1031 assert_eq!(events.len(), 1);
1032 assert_eq!(events[0].severity, Severity::High);
1033 let ids: Vec<&str> = events[0]
1034 .mitre_attack
1035 .iter()
1036 .map(memf_correlate::mitre::MitreAttackId::as_str)
1037 .collect();
1038 assert!(ids.contains(&"T1014"), "expected T1014, got {ids:?}");
1039 assert!((events[0].confidence - 0.85).abs() < 1e-9);
1040 }
1041
1042 #[test]
1043 fn all_structures_present_is_info() {
1044 let h = make_hidden_process(100, "normal", true, true, true);
1045 let events = h.into_forensic_events();
1046 assert_eq!(events.len(), 1);
1047 assert_eq!(events[0].severity, Severity::Info);
1048 }
1049
1050 #[test]
1051 fn source_walker_is_linux_proc_hidden() {
1052 let h = make_hidden_process(1, "init", true, true, true);
1053 let events = h.into_forensic_events();
1054 assert_eq!(events[0].source_walker, "linux_proc_hidden");
1055 }
1056
1057 fn make_vdso(pid: u64, comm: &str, differs: bool, diff_byte_count: usize) -> VdsoTamperInfo {
1062 VdsoTamperInfo {
1063 pid,
1064 comm: comm.to_string(),
1065 vdso_base: 0x7fff_f000_0000,
1066 vdso_size: 0x2000,
1067 differs_from_canonical: differs,
1068 diff_byte_count,
1069 }
1070 }
1071
1072 #[test]
1073 fn large_vdso_diff_is_critical() {
1074 let v = make_vdso(1234, "evil", true, 32);
1075 let events = v.into_forensic_events();
1076 assert_eq!(events.len(), 1);
1077 assert_eq!(events[0].severity, Severity::Critical);
1078 let ids: Vec<&str> = events[0]
1079 .mitre_attack
1080 .iter()
1081 .map(memf_correlate::mitre::MitreAttackId::as_str)
1082 .collect();
1083 assert!(ids.contains(&"T1055"), "expected T1055, got {ids:?}");
1084 assert!((events[0].confidence - 0.95).abs() < 1e-9);
1085 assert!(matches!(events[0].finding, Finding::ProcessHollowing));
1086 }
1087
1088 #[test]
1089 fn small_vdso_diff_is_high() {
1090 let v = make_vdso(2345, "sneaky", true, 8);
1091 let events = v.into_forensic_events();
1092 assert_eq!(events.len(), 1);
1093 assert_eq!(events[0].severity, Severity::High);
1094 let ids: Vec<&str> = events[0]
1095 .mitre_attack
1096 .iter()
1097 .map(memf_correlate::mitre::MitreAttackId::as_str)
1098 .collect();
1099 assert!(ids.contains(&"T1055"), "expected T1055, got {ids:?}");
1100 assert!((events[0].confidence - 0.8).abs() < 1e-9);
1101 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
1102 }
1103
1104 #[test]
1105 fn clean_vdso_is_info() {
1106 let v = make_vdso(42, "bash", false, 0);
1107 let events = v.into_forensic_events();
1108 assert_eq!(events.len(), 1);
1109 assert_eq!(events[0].severity, Severity::Info);
1110 }
1111
1112 #[test]
1113 fn tampered_vdso_has_t1055() {
1114 let v = make_vdso(77, "sshd", true, 100);
1115 let events = v.into_forensic_events();
1116 let ids: Vec<&str> = events[0]
1117 .mitre_attack
1118 .iter()
1119 .map(memf_correlate::mitre::MitreAttackId::as_str)
1120 .collect();
1121 assert!(
1122 ids.contains(&"T1055"),
1123 "expected T1055 for tampered vDSO, got {ids:?}"
1124 );
1125 }
1126
1127 #[test]
1128 fn source_walker_is_linux_vdso_tamper() {
1129 let v = make_vdso(1, "init", false, 0);
1130 let events = v.into_forensic_events();
1131 assert_eq!(events[0].source_walker, "linux_vdso_tamper");
1132 }
1133
1134 fn make_user_ns(
1139 pid: u64,
1140 comm: &str,
1141 ns_depth: u32,
1142 owner_uid: u32,
1143 process_uid: u32,
1144 has_cap_sys_admin: bool,
1145 ) -> UserNsEscalationInfo {
1146 UserNsEscalationInfo {
1147 pid,
1148 comm: comm.to_string(),
1149 ns_depth,
1150 owner_uid,
1151 process_uid,
1152 has_cap_sys_admin,
1153 is_suspicious: false,
1154 }
1155 }
1156
1157 #[test]
1158 fn cap_sys_admin_with_different_uid_is_critical() {
1159 let u = make_user_ns(1234, "evil", 1, 0, 1000, true);
1160 let events = u.into_forensic_events();
1161 assert_eq!(events.len(), 1);
1162 assert_eq!(events[0].severity, Severity::Critical);
1163 let ids: Vec<&str> = events[0]
1164 .mitre_attack
1165 .iter()
1166 .map(memf_correlate::mitre::MitreAttackId::as_str)
1167 .collect();
1168 assert!(ids.contains(&"T1611"), "expected T1611, got {ids:?}");
1169 assert!((events[0].confidence - 0.9).abs() < 1e-9);
1170 }
1171
1172 #[test]
1173 fn deep_namespace_nesting_is_high() {
1174 let u = make_user_ns(555, "nested", 5, 1000, 1000, false);
1175 let events = u.into_forensic_events();
1176 assert_eq!(events.len(), 1);
1177 assert_eq!(events[0].severity, Severity::High);
1178 let ids: Vec<&str> = events[0]
1179 .mitre_attack
1180 .iter()
1181 .map(memf_correlate::mitre::MitreAttackId::as_str)
1182 .collect();
1183 assert!(ids.contains(&"T1611"), "expected T1611, got {ids:?}");
1184 assert!((events[0].confidence - 0.7).abs() < 1e-9);
1185 }
1186
1187 #[test]
1188 fn root_owned_cap_admin_is_medium() {
1189 let u = make_user_ns(777, "daemon", 1, 0, 0, true);
1191 let events = u.into_forensic_events();
1192 assert_eq!(events.len(), 1);
1193 assert_eq!(events[0].severity, Severity::Medium);
1194 let ids: Vec<&str> = events[0]
1195 .mitre_attack
1196 .iter()
1197 .map(memf_correlate::mitre::MitreAttackId::as_str)
1198 .collect();
1199 assert!(ids.contains(&"T1548"), "expected T1548, got {ids:?}");
1200 assert!((events[0].confidence - 0.6).abs() < 1e-9);
1201 }
1202
1203 #[test]
1204 fn normal_namespace_is_info() {
1205 let u = make_user_ns(100, "bash", 1, 1000, 1000, false);
1206 let events = u.into_forensic_events();
1207 assert_eq!(events.len(), 1);
1208 assert_eq!(events[0].severity, Severity::Info);
1209 }
1210
1211 #[test]
1212 fn source_walker_is_linux_user_ns() {
1213 let u = make_user_ns(1, "init", 0, 0, 0, false);
1214 let events = u.into_forensic_events();
1215 assert_eq!(events[0].source_walker, "linux_user_ns");
1216 }
1217
1218 fn make_audit(
1223 audit_enabled: bool,
1224 backlog_limit: u32,
1225 suppressed_pids: Vec<u64>,
1226 audit_globally_disabled: bool,
1227 ) -> AuditTamperInfo {
1228 AuditTamperInfo {
1229 audit_enabled,
1230 backlog_limit,
1231 suppressed_pids,
1232 suppressed_uids: vec![],
1233 audit_globally_disabled,
1234 }
1235 }
1236
1237 #[test]
1238 fn globally_disabled_audit_is_critical() {
1239 let a = make_audit(false, 256, vec![], true);
1240 let events = a.into_forensic_events();
1241 assert_eq!(events.len(), 1);
1242 assert_eq!(events[0].severity, Severity::Critical);
1243 let ids: Vec<&str> = events[0]
1244 .mitre_attack
1245 .iter()
1246 .map(memf_correlate::mitre::MitreAttackId::as_str)
1247 .collect();
1248 assert!(ids.contains(&"T1562"), "expected T1562, got {ids:?}");
1249 assert!((events[0].confidence - 0.95).abs() < 1e-9);
1250 }
1251
1252 #[test]
1253 fn suppressed_pid_is_high() {
1254 let a = make_audit(true, 256, vec![1234], false);
1255 let events = a.into_forensic_events();
1256 assert_eq!(events.len(), 1);
1257 assert_eq!(events[0].severity, Severity::High);
1258 let ids: Vec<&str> = events[0]
1259 .mitre_attack
1260 .iter()
1261 .map(memf_correlate::mitre::MitreAttackId::as_str)
1262 .collect();
1263 assert!(ids.contains(&"T1562"), "expected T1562, got {ids:?}");
1264 assert!((events[0].confidence - 0.85).abs() < 1e-9);
1265 }
1266
1267 #[test]
1268 fn low_backlog_limit_is_medium() {
1269 let a = make_audit(true, 32, vec![], false);
1270 let events = a.into_forensic_events();
1271 assert_eq!(events.len(), 1);
1272 assert_eq!(events[0].severity, Severity::Medium);
1273 let ids: Vec<&str> = events[0]
1274 .mitre_attack
1275 .iter()
1276 .map(memf_correlate::mitre::MitreAttackId::as_str)
1277 .collect();
1278 assert!(ids.contains(&"T1562"), "expected T1562, got {ids:?}");
1279 assert!((events[0].confidence - 0.6).abs() < 1e-9);
1280 }
1281
1282 #[test]
1283 fn normal_audit_is_info() {
1284 let a = make_audit(true, 256, vec![], false);
1285 let events = a.into_forensic_events();
1286 assert_eq!(events.len(), 1);
1287 assert_eq!(events[0].severity, Severity::Info);
1288 }
1289
1290 #[test]
1291 fn source_walker_is_linux_netlink_audit() {
1292 let a = make_audit(true, 256, vec![], false);
1293 let events = a.into_forensic_events();
1294 assert_eq!(events[0].source_walker, "linux_netlink_audit");
1295 }
1296
1297 fn make_cpu_pinning(
1302 pid: u64,
1303 comm: &str,
1304 pinned_cpu_count: u32,
1305 total_cpu_count: u32,
1306 sched_policy: u32,
1307 cpu_time_ns: u64,
1308 ) -> CpuPinningInfo {
1309 CpuPinningInfo {
1310 pid,
1311 comm: comm.to_string(),
1312 pinned_cpu_count,
1313 total_cpu_count,
1314 sched_policy,
1315 cpu_time_ns,
1316 }
1317 }
1318
1319 #[test]
1320 fn single_cpu_pinned_with_high_cpu_time_is_high() {
1321 let c = make_cpu_pinning(1234, "miner", 1, 8, 0, 2_000_000_000);
1322 let events = c.into_forensic_events();
1323 assert_eq!(events.len(), 1);
1324 assert_eq!(events[0].severity, Severity::High);
1325 let ids: Vec<&str> = events[0]
1326 .mitre_attack
1327 .iter()
1328 .map(memf_correlate::mitre::MitreAttackId::as_str)
1329 .collect();
1330 assert!(ids.contains(&"T1496"), "expected T1496, got {ids:?}");
1331 assert!((events[0].confidence - 0.8).abs() < 1e-9);
1332 }
1333
1334 #[test]
1335 fn batch_scheduling_is_medium() {
1336 let c = make_cpu_pinning(2345, "bgworker", 4, 8, 3, 100_000_000);
1337 let events = c.into_forensic_events();
1338 assert_eq!(events.len(), 1);
1339 assert_eq!(events[0].severity, Severity::Medium);
1340 let ids: Vec<&str> = events[0]
1341 .mitre_attack
1342 .iter()
1343 .map(memf_correlate::mitre::MitreAttackId::as_str)
1344 .collect();
1345 assert!(ids.contains(&"T1496"), "expected T1496, got {ids:?}");
1346 assert!((events[0].confidence - 0.5).abs() < 1e-9);
1347 }
1348
1349 #[test]
1350 fn normal_process_is_info_cpu() {
1351 let c = make_cpu_pinning(42, "bash", 4, 8, 0, 50_000_000);
1352 let events = c.into_forensic_events();
1353 assert_eq!(events.len(), 1);
1354 assert_eq!(events[0].severity, Severity::Info);
1355 }
1356
1357 #[test]
1358 fn source_walker_is_linux_cpu_pinning() {
1359 let c = make_cpu_pinning(1, "init", 4, 8, 0, 0);
1360 let events = c.into_forensic_events();
1361 assert_eq!(events[0].source_walker, "linux_cpu_pinning");
1362 }
1363
1364 #[allow(clippy::fn_params_excessive_bools)]
1370 fn make_container_escape(
1371 pid: u64,
1372 comm: &str,
1373 pid_ns_differs_from_cgroup_ns: bool,
1374 has_host_mounts: bool,
1375 cap_sys_admin: bool,
1376 in_non_init_pid_ns: bool,
1377 ) -> ContainerEscapeCorrelateInfo {
1378 ContainerEscapeCorrelateInfo {
1379 pid,
1380 comm: comm.to_string(),
1381 pid_ns_differs_from_cgroup_ns,
1382 has_host_mounts,
1383 cap_sys_admin,
1384 cap_sys_ptrace: false,
1385 in_non_init_pid_ns,
1386 }
1387 }
1388
1389 #[test]
1390 fn host_mounts_in_container_is_critical() {
1391 let c = make_container_escape(1234, "evil", false, true, false, true);
1392 let events = c.into_forensic_events();
1393 assert_eq!(events.len(), 1);
1394 assert_eq!(events[0].severity, Severity::Critical);
1395 let ids: Vec<&str> = events[0]
1396 .mitre_attack
1397 .iter()
1398 .map(memf_correlate::mitre::MitreAttackId::as_str)
1399 .collect();
1400 assert!(ids.contains(&"T1611"), "expected T1611, got {ids:?}");
1401 assert!((events[0].confidence - 0.9).abs() < 1e-9);
1402 }
1403
1404 #[test]
1405 fn cap_sys_admin_in_container_is_high() {
1406 let c = make_container_escape(2345, "priv", false, false, true, true);
1407 let events = c.into_forensic_events();
1408 assert_eq!(events.len(), 1);
1409 assert_eq!(events[0].severity, Severity::High);
1410 let ids: Vec<&str> = events[0]
1411 .mitre_attack
1412 .iter()
1413 .map(memf_correlate::mitre::MitreAttackId::as_str)
1414 .collect();
1415 assert!(ids.contains(&"T1611"), "expected T1611, got {ids:?}");
1416 assert!((events[0].confidence - 0.8).abs() < 1e-9);
1417 }
1418
1419 #[test]
1420 fn pid_cgroup_ns_mismatch_is_high() {
1421 let c = make_container_escape(3456, "ns_mismatch", true, false, false, false);
1422 let events = c.into_forensic_events();
1423 assert_eq!(events.len(), 1);
1424 assert_eq!(events[0].severity, Severity::High);
1425 let ids: Vec<&str> = events[0]
1426 .mitre_attack
1427 .iter()
1428 .map(memf_correlate::mitre::MitreAttackId::as_str)
1429 .collect();
1430 assert!(ids.contains(&"T1611"), "expected T1611, got {ids:?}");
1431 assert!((events[0].confidence - 0.75).abs() < 1e-9);
1432 }
1433
1434 #[test]
1435 fn normal_container_process_is_info() {
1436 let c = make_container_escape(100, "nginx", false, false, false, true);
1437 let events = c.into_forensic_events();
1438 assert_eq!(events.len(), 1);
1439 assert_eq!(events[0].severity, Severity::Info);
1440 }
1441
1442 #[test]
1443 fn source_walker_is_linux_container_escape() {
1444 let c = make_container_escape(1, "init", false, false, false, false);
1445 let events = c.into_forensic_events();
1446 assert_eq!(events[0].source_walker, "linux_container_escape");
1447 }
1448
1449 fn make_fd_abuse(
1454 pid: u64,
1455 comm: &str,
1456 fd_type: FdAbuseType,
1457 signal_mask: u64,
1458 interval_ns: u64,
1459 is_cross_process_shared: bool,
1460 ) -> FdAbuseInfo {
1461 FdAbuseInfo {
1462 pid,
1463 comm: comm.to_string(),
1464 fd_type,
1465 signal_mask,
1466 interval_ns,
1467 is_cross_process_shared,
1468 }
1469 }
1470
1471 #[test]
1472 fn sigterm_intercepting_signalfd_is_high() {
1473 let f = make_fd_abuse(1234, "evil", FdAbuseType::SignalFd, 1u64 << 15, 0, false);
1475 let events = f.into_forensic_events();
1476 assert_eq!(events.len(), 1);
1477 assert_eq!(events[0].severity, Severity::High);
1478 let ids: Vec<&str> = events[0]
1479 .mitre_attack
1480 .iter()
1481 .map(memf_correlate::mitre::MitreAttackId::as_str)
1482 .collect();
1483 assert!(ids.contains(&"T1205"), "expected T1205, got {ids:?}");
1484 assert!((events[0].confidence - 0.8).abs() < 1e-9);
1485 }
1486
1487 #[test]
1488 fn subsecond_timerfd_is_medium() {
1489 let f = make_fd_abuse(2345, "beacon", FdAbuseType::TimerFd, 0, 500_000_000, false);
1490 let events = f.into_forensic_events();
1491 assert_eq!(events.len(), 1);
1492 assert_eq!(events[0].severity, Severity::Medium);
1493 let ids: Vec<&str> = events[0]
1494 .mitre_attack
1495 .iter()
1496 .map(memf_correlate::mitre::MitreAttackId::as_str)
1497 .collect();
1498 assert!(ids.contains(&"T1071"), "expected T1071, got {ids:?}");
1499 assert!((events[0].confidence - 0.5).abs() < 1e-9);
1500 }
1501
1502 #[test]
1503 fn cross_process_eventfd_is_medium() {
1504 let f = make_fd_abuse(3456, "shared", FdAbuseType::EventFd, 0, 5_000_000_000, true);
1505 let events = f.into_forensic_events();
1506 assert_eq!(events.len(), 1);
1507 assert_eq!(events[0].severity, Severity::Medium);
1508 let ids: Vec<&str> = events[0]
1509 .mitre_attack
1510 .iter()
1511 .map(memf_correlate::mitre::MitreAttackId::as_str)
1512 .collect();
1513 assert!(ids.contains(&"T1071"), "expected T1071, got {ids:?}");
1514 assert!((events[0].confidence - 0.6).abs() < 1e-9);
1515 }
1516
1517 #[test]
1518 fn normal_timerfd_is_info() {
1519 let f = make_fd_abuse(42, "cron", FdAbuseType::TimerFd, 0, 60_000_000_000, false);
1520 let events = f.into_forensic_events();
1521 assert_eq!(events.len(), 1);
1522 assert_eq!(events[0].severity, Severity::Info);
1523 }
1524
1525 #[test]
1526 fn source_walker_is_linux_timerfd_signalfd() {
1527 let f = make_fd_abuse(1, "init", FdAbuseType::TimerFd, 0, 0, false);
1528 let events = f.into_forensic_events();
1529 assert_eq!(events[0].source_walker, "linux_timerfd_signalfd");
1530 }
1531
1532 #[allow(clippy::fn_params_excessive_bools)]
1538 fn make_shared_mem(
1539 pid: u64,
1540 comm: &str,
1541 is_memfd: bool,
1542 is_executable: bool,
1543 is_cross_uid: bool,
1544 has_elf_header: bool,
1545 ) -> SharedMemAnomalyInfo {
1546 SharedMemAnomalyInfo {
1547 pid,
1548 comm: comm.to_string(),
1549 shm_base: 0x7f00_0000_0000,
1550 shm_size: 0x1000,
1551 is_memfd,
1552 is_executable,
1553 is_cross_uid,
1554 has_elf_header,
1555 }
1556 }
1557
1558 #[test]
1559 fn executable_memfd_is_critical() {
1560 let s = make_shared_mem(1234, "loader", true, true, false, false);
1561 let events = s.into_forensic_events();
1562 assert_eq!(events.len(), 1);
1563 assert_eq!(events[0].severity, Severity::Critical);
1564 let ids: Vec<&str> = events[0]
1565 .mitre_attack
1566 .iter()
1567 .map(memf_correlate::mitre::MitreAttackId::as_str)
1568 .collect();
1569 assert!(ids.contains(&"T1027"), "expected T1027, got {ids:?}");
1570 assert!((events[0].confidence - 0.9).abs() < 1e-9);
1571 assert!(matches!(events[0].finding, Finding::ProcessHollowing));
1572 }
1573
1574 #[test]
1575 fn executable_region_with_elf_header_is_high() {
1576 let s = make_shared_mem(2345, "injector", false, true, false, true);
1577 let events = s.into_forensic_events();
1578 assert_eq!(events.len(), 1);
1579 assert_eq!(events[0].severity, Severity::High);
1580 let ids: Vec<&str> = events[0]
1581 .mitre_attack
1582 .iter()
1583 .map(memf_correlate::mitre::MitreAttackId::as_str)
1584 .collect();
1585 assert!(ids.contains(&"T1055"), "expected T1055, got {ids:?}");
1586 assert!((events[0].confidence - 0.85).abs() < 1e-9);
1587 assert!(matches!(events[0].finding, Finding::ProcessHollowing));
1588 }
1589
1590 #[test]
1591 fn cross_uid_shared_mem_is_medium() {
1592 let s = make_shared_mem(3456, "ipc", false, false, true, false);
1593 let events = s.into_forensic_events();
1594 assert_eq!(events.len(), 1);
1595 assert_eq!(events[0].severity, Severity::Medium);
1596 let ids: Vec<&str> = events[0]
1597 .mitre_attack
1598 .iter()
1599 .map(memf_correlate::mitre::MitreAttackId::as_str)
1600 .collect();
1601 assert!(ids.contains(&"T1055"), "expected T1055, got {ids:?}");
1602 assert!((events[0].confidence - 0.6).abs() < 1e-9);
1603 assert!(matches!(events[0].finding, Finding::DefenseEvasion));
1604 }
1605
1606 #[test]
1607 fn normal_shared_mem_is_info() {
1608 let s = make_shared_mem(42, "postgres", false, false, false, false);
1609 let events = s.into_forensic_events();
1610 assert_eq!(events.len(), 1);
1611 assert_eq!(events[0].severity, Severity::Info);
1612 }
1613
1614 #[test]
1615 fn source_walker_is_linux_shared_mem() {
1616 let s = make_shared_mem(1, "init", false, false, false, false);
1617 let events = s.into_forensic_events();
1618 assert_eq!(events[0].source_walker, "linux_shared_mem");
1619 }
1620
1621 fn make_fuse(
1626 pid: u64,
1627 comm: &str,
1628 mount_point: &str,
1629 is_over_sensitive_path: bool,
1630 daemon_is_root: bool,
1631 allow_other: bool,
1632 ) -> FuseAbuseInfo {
1633 FuseAbuseInfo {
1634 pid,
1635 comm: comm.to_string(),
1636 mount_point: mount_point.to_string(),
1637 is_over_sensitive_path,
1638 daemon_is_root,
1639 allow_other,
1640 }
1641 }
1642
1643 #[test]
1644 fn fuse_over_sensitive_path_is_high() {
1645 let f = make_fuse(1234, "fusermount", "/proc", true, false, false);
1646 let events = f.into_forensic_events();
1647 assert_eq!(events.len(), 1);
1648 assert_eq!(events[0].severity, Severity::High);
1649 let ids: Vec<&str> = events[0]
1650 .mitre_attack
1651 .iter()
1652 .map(memf_correlate::mitre::MitreAttackId::as_str)
1653 .collect();
1654 assert!(ids.contains(&"T1564"), "expected T1564, got {ids:?}");
1655 assert!((events[0].confidence - 0.9).abs() < 1e-9);
1656 }
1657
1658 #[test]
1659 fn root_fuse_with_allow_other_is_medium() {
1660 let f = make_fuse(2345, "sshfs", "/mnt/data", false, true, true);
1661 let events = f.into_forensic_events();
1662 assert_eq!(events.len(), 1);
1663 assert_eq!(events[0].severity, Severity::Medium);
1664 let ids: Vec<&str> = events[0]
1665 .mitre_attack
1666 .iter()
1667 .map(memf_correlate::mitre::MitreAttackId::as_str)
1668 .collect();
1669 assert!(ids.contains(&"T1564"), "expected T1564, got {ids:?}");
1670 assert!((events[0].confidence - 0.6).abs() < 1e-9);
1671 }
1672
1673 #[test]
1674 fn normal_fuse_mount_is_info() {
1675 let f = make_fuse(42, "sshfs", "/home/user/remote", false, false, false);
1676 let events = f.into_forensic_events();
1677 assert_eq!(events.len(), 1);
1678 assert_eq!(events[0].severity, Severity::Info);
1679 }
1680
1681 #[test]
1682 fn source_walker_is_linux_fuse() {
1683 let f = make_fuse(1, "fusermount", "/mnt", false, false, false);
1684 let events = f.into_forensic_events();
1685 assert_eq!(events[0].source_walker, "linux_fuse");
1686 }
1687}