Skip to main content

memf_linux/
correlate.rs

1//! [`IntoForensicEvents`] implementations for Linux walker output types.
2
3use 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                // RWX anonymous mapping — classic shellcode/injection pattern (T1055)
18                (
19                    Severity::High,
20                    Finding::ProcessHollowing,
21                    MitreAttackId::vec("T1055"),
22                    0.9f64,
23                )
24            } else if self.flags.exec && !self.file_backed {
25                // Executable anonymous mapping — JIT or shellcode, less certain (T1055)
26                (
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            // Blank comm — hidden / name-erased process (T1564)
60            (
61                Severity::High,
62                Finding::DefenseEvasion,
63                MitreAttackId::vec("T1564"),
64                0.9f64,
65            )
66        } else if self.state == ProcessState::Zombie {
67            // Zombie process — possible evasion indicator (T1564)
68            (
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            // Kernel thread with non-zero ppid — suspicious kthread
76            (
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                // Classic C2 ports (T1071)
115                (
116                    Severity::High,
117                    Finding::NetworkBeaconing,
118                    MitreAttackId::vec("T1071"),
119                    0.8f64,
120                )
121            } else if self.pid.is_none() {
122                // No owning process — hidden connection (T1095)
123                (
124                    Severity::High,
125                    Finding::DefenseEvasion,
126                    MitreAttackId::vec("T1095"),
127                    0.85f64,
128                )
129            } else if self.remote_port == 0 && !is_loopback {
130                // Port 0 with non-loopback remote — suspicious beaconing (T1071)
131                (
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            // Blank module name — hidden kernel module (T1014)
172            (
173                Severity::High,
174                Finding::DefenseEvasion,
175                MitreAttackId::vec("T1014"),
176                0.9f64,
177            )
178        } else if matches!(self.state, ModuleState::Going) {
179            // Module unloading during scan — possible hide-by-unload evasion (T1014)
180            (
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
210// ---------------------------------------------------------------------------
211// Batch 1: proc_hidden, vdso_tamper, user_ns_escalation, netlink_audit, cpu_pinning
212// ---------------------------------------------------------------------------
213
214impl 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                // Visible in PID namespace but not in task list — classic DKOM rootkit (T1014)
219                (
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                // In PID hash but not task list — partial DKOM (T1014)
227                (
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                // In task list but not PID namespace — namespace hiding (T1014)
235                (
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                // Large vDSO diff — likely patched syscall stubs (T1055)
270                (
271                    Severity::Critical,
272                    Finding::ProcessHollowing,
273                    MitreAttackId::vec("T1055"),
274                    0.95f64,
275                )
276            } else if self.differs_from_canonical {
277                // Small vDSO diff — possible targeted patch (T1055)
278                (
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                // CAP_SYS_ADMIN mapped for a different UID — privilege escalation (T1611)
313                (
314                    Severity::Critical,
315                    Finding::DefenseEvasion,
316                    MitreAttackId::vec("T1611"),
317                    0.9f64,
318                )
319            } else if self.ns_depth > 3 {
320                // Deeply nested user namespace — evasion technique (T1611)
321                (
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                // Root-owned namespace with CAP_SYS_ADMIN (T1548)
329                (
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            // Audit subsystem globally disabled — Defense Evasion (T1562)
363            (
364                Severity::Critical,
365                Finding::DefenseEvasion,
366                MitreAttackId::vec("T1562"),
367                0.95f64,
368            )
369        } else if !self.suppressed_pids.is_empty() {
370            // PIDs excluded from auditing — targeted evasion (T1562)
371            (
372                Severity::High,
373                Finding::DefenseEvasion,
374                MitreAttackId::vec("T1562"),
375                0.85f64,
376            )
377        } else if self.backlog_limit < 64 {
378            // Very low backlog limit — audit log flooding / evasion (T1562)
379            (
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                // Single CPU affinity with high CPU consumption — cryptominer pattern (T1496)
412                (
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                // SCHED_BATCH or SCHED_IDLE — stealth scheduling (T1496)
420                (
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
450// ---------------------------------------------------------------------------
451// Batch 2: container_escape, timerfd_signalfd, shared_mem_anomaly, fuse_abuse
452// ---------------------------------------------------------------------------
453
454impl 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                // Host filesystem mounts visible from within a container (T1611)
459                (
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                // CAP_SYS_ADMIN inside a container namespace (T1611)
467                (
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                // PID/cgroup namespace mismatch — suspicious escape attempt (T1611)
475                (
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                // signalfd intercepting SIGTERM — Defense Evasion (T1205)
510                (
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                // Sub-second timerfd interval — potential beaconing (T1071)
518                (
519                    Severity::Medium,
520                    Finding::NetworkBeaconing,
521                    MitreAttackId::vec("T1071"),
522                    0.5f64,
523                )
524            } else if self.is_cross_process_shared {
525                // Cross-process fd sharing — covert channel (T1071)
526                (
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            // Executable memfd — in-memory code execution (T1027)
560            (
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            // Executable shared region with ELF header — process injection (T1055)
568            (
569                Severity::High,
570                Finding::ProcessHollowing,
571                MitreAttackId::vec("T1055"),
572                0.85f64,
573            )
574        } else if self.is_cross_uid {
575            // Cross-UID shared memory — possible privilege escalation vector (T1055)
576            (
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            // FUSE mounted over /proc, /sys, /etc — Hide Artifacts (T1564)
610            (
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            // Root FUSE daemon with allow_other — privilege escalation risk (T1564)
618            (
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        // exec + no write + no file backing: suspicious but less so (JIT, shellcode)
686        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    // -----------------------------------------------------------------------
749    // ProcessInfo tests
750    // -----------------------------------------------------------------------
751
752    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    // -----------------------------------------------------------------------
833    // ConnectionInfo tests
834    // -----------------------------------------------------------------------
835
836    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    // -----------------------------------------------------------------------
920    // ModuleInfo tests
921    // -----------------------------------------------------------------------
922
923    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        // MODULE_STATE_GOING during scan — possible unload-to-hide evasion (T1014)
943        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        // Blank module name — hidden kernel module (T1014)
958        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    // -----------------------------------------------------------------------
977    // HiddenProcessInfo tests
978    // -----------------------------------------------------------------------
979
980    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    // -----------------------------------------------------------------------
1058    // VdsoTamperInfo tests
1059    // -----------------------------------------------------------------------
1060
1061    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    // -----------------------------------------------------------------------
1135    // UserNsEscalationInfo tests
1136    // -----------------------------------------------------------------------
1137
1138    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        // owner_uid == 0 && has_cap_sys_admin && owner_uid == process_uid → Medium T1548
1190        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    // -----------------------------------------------------------------------
1219    // AuditTamperInfo tests
1220    // -----------------------------------------------------------------------
1221
1222    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    // -----------------------------------------------------------------------
1298    // CpuPinningInfo tests
1299    // -----------------------------------------------------------------------
1300
1301    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    // -----------------------------------------------------------------------
1365    // ContainerEscapeCorrelateInfo tests
1366    // -----------------------------------------------------------------------
1367
1368    // Test fixture builder: each bool maps 1:1 to a struct field, named at the call site.
1369    #[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    // -----------------------------------------------------------------------
1450    // FdAbuseInfo tests
1451    // -----------------------------------------------------------------------
1452
1453    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        // SIGTERM = signal 15, bit 15 = 1u64 << 15
1474        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    // -----------------------------------------------------------------------
1533    // SharedMemAnomalyInfo tests
1534    // -----------------------------------------------------------------------
1535
1536    // Test fixture builder: each bool maps 1:1 to a struct field, named at the call site.
1537    #[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    // -----------------------------------------------------------------------
1622    // FuseAbuseInfo tests
1623    // -----------------------------------------------------------------------
1624
1625    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}