1pub fn classify_bpf_program(prog_type: &str, name: &str) -> bool {
21 match prog_type {
22 "tracing" | "raw_tracepoint" => name.is_empty(),
24
25 "kprobe" | "raw_tracepoint_writable" | "lsm" => true,
29
30 _ => false,
33 }
34}
35
36const CAP_NET_RAW: u64 = 1 << 13;
42const CAP_SYS_MODULE: u64 = 1 << 16;
43const CAP_SYS_PTRACE: u64 = 1 << 19;
44const CAP_SYS_ADMIN: u64 = 1 << 21;
45
46const SUSPICIOUS_CAPS: &[(u64, &str)] = &[
48 (CAP_SYS_ADMIN, "CAP_SYS_ADMIN"),
49 (CAP_SYS_PTRACE, "CAP_SYS_PTRACE"),
50 (CAP_SYS_MODULE, "CAP_SYS_MODULE"),
51 (CAP_NET_RAW, "CAP_NET_RAW"),
52];
53
54pub fn classify_capabilities(effective: u64, uid: u32) -> (bool, Vec<String>) {
59 if uid == 0 {
61 return (false, Vec::new());
62 }
63
64 let mut suspicious_names = Vec::new();
65 for &(cap_bit, cap_label) in SUSPICIOUS_CAPS {
66 if effective & cap_bit != 0 {
67 suspicious_names.push(cap_label.to_string());
68 }
69 }
70
71 let is_suspicious = !suspicious_names.is_empty();
72 (is_suspicious, suspicious_names)
73}
74
75pub fn classify_cgroup(path: &str) -> (bool, String) {
84 const RUNTIME_PREFIXES: &[&str] = &["/docker/", "/lxc/", "/kubepods/", "/containerd/"];
85
86 for prefix in RUNTIME_PREFIXES {
87 if let Some(idx) = path.find(prefix) {
88 let after_prefix = &path[idx + prefix.len()..];
89 let id = after_prefix.split('/').next().unwrap_or("").to_string();
91 return (true, id);
92 }
93 }
94
95 (false, String::new())
96}
97
98pub fn classify_afinfo_hook(hook_addr: u64, kernel_start: u64, kernel_end: u64) -> bool {
107 if hook_addr == 0 {
108 return false;
109 }
110 !(kernel_start <= hook_addr && hook_addr <= kernel_end)
111}
112
113fn is_likely_kernel_thread_heuristic(pid: u32) -> bool {
119 pid <= 2
120}
121
122pub fn classify_shared_creds(pid: u32, shared_with: &[u32], uid: u32) -> bool {
127 if shared_with.contains(&1) && pid != 1 {
129 if uid == 0 && is_likely_kernel_thread_heuristic(pid) {
131 return false;
132 }
133 return true;
134 }
135
136 if uid == 0 && is_likely_kernel_thread_heuristic(pid) {
138 return false;
139 }
140
141 !shared_with.is_empty()
143}
144
145pub fn classify_idt_entry(handler_addr: u64, kernel_start: u64, kernel_end: u64) -> bool {
153 if handler_addr == 0 {
154 return false;
155 }
156 !(kernel_start <= handler_addr && handler_addr <= kernel_end)
157}
158
159const KERNEL_THREAD_COMMS: &[&str] = &["kthread", "kworker", "migration", "ksoftirqd", "rcu_"];
165
166pub fn classify_container_escape(comm: &str, indicator: &str) -> bool {
170 let is_kernel = KERNEL_THREAD_COMMS
171 .iter()
172 .any(|prefix| comm.starts_with(prefix));
173 if is_kernel {
174 return false;
175 }
176 matches!(indicator, "namespace_mismatch" | "host_mount_access")
177}
178
179const KNOWN_BENIGN_COMMS: &[&str] = &[
185 "apt",
186 "apt-get",
187 "apt-check",
188 "aptd",
189 "dpkg",
190 "dpkg-deb",
191 "yum",
192 "dnf",
193 "rpm",
194 "rpmdb",
195 "packagekitd",
196 "unattended-upgr",
197];
198
199pub fn classify_deleted_exe(exe_path: &str, comm: &str) -> bool {
204 if !exe_path.contains("(deleted)") {
206 return false;
207 }
208
209 if exe_path.is_empty() {
211 return false;
212 }
213
214 if comm.is_empty() {
216 return false;
217 }
218
219 let comm_lower = comm.to_lowercase();
221 for &benign in KNOWN_BENIGN_COMMS {
222 if comm_lower == benign {
223 return false;
224 }
225 }
226
227 true
229}
230
231const SUSPICIOUS_EXTENSIONS: &[&str] = &[".so", ".py", ".sh", ".elf", ".bin"];
237
238pub fn classify_hidden_dentry(nlink: u32, filename: &str) -> bool {
243 if filename.is_empty() {
245 return false;
246 }
247
248 let name_lower = filename.to_lowercase();
249
250 if nlink > 0 {
252 return SUSPICIOUS_EXTENSIONS
253 .iter()
254 .any(|ext| name_lower.ends_with(ext));
255 }
256
257 true
259}
260
261const SUSPICIOUS_MAP_NAMES: &[&str] = &[
267 "rootkit",
268 "hide_",
269 "hook",
270 "intercept",
271 "stealth",
272 "secret",
273 "covert",
274 "keylog",
275 "exfil",
276];
277
278pub fn classify_ebpf_map(map_type: u32, name: &str, _value_size: u32) -> bool {
283 let name_lower = name.to_lowercase();
284 let suspicious_name = SUSPICIOUS_MAP_NAMES.iter().any(|p| name_lower.contains(p));
285
286 let high_risk_type = matches!(map_type, 3 | 26);
288
289 suspicious_name || high_risk_type
290}
291
292pub fn classify_ftrace_hook(func: u64, stext: u64, etext: u64) -> bool {
300 func < stext || func >= etext
301}
302
303pub fn classify_futex(key_address: u64, owner_pid: u32, waiter_count: u32) -> bool {
312 waiter_count > 1000 || (key_address > 0x7FFF_FFFF_FFFF && owner_pid > 0)
313}
314
315const IORING_OP_SENDMSG: u8 = 9;
321const IORING_OP_RECVMSG: u8 = 10;
323const IORING_OP_CONNECT: u8 = 16;
325
326const SENSITIVE_OPCODES: &[u8] = &[IORING_OP_SENDMSG, IORING_OP_RECVMSG, IORING_OP_CONNECT];
328
329pub fn classify_io_uring(opcodes: &[u8], seccomp_mode: u32) -> bool {
334 if seccomp_mode == 0 {
335 return false;
336 }
337 opcodes.iter().any(|op| SENSITIVE_OPCODES.contains(op))
338}
339
340pub fn classify_iomem(name: &str, start: u64, end: u64) -> bool {
349 let size = end.saturating_sub(start);
351 if name.is_empty() && size > 1024 * 1024 {
352 return true;
353 }
354
355 if name.chars().any(|c| c.is_control() || !c.is_ascii()) {
357 return true;
358 }
359
360 #[allow(clippy::items_after_statements)]
362 const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
363 #[allow(clippy::items_after_statements)]
364 const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
365 if start < KERNEL_TEXT_END && end > KERNEL_TEXT_START && name != "Kernel code" {
366 return true;
367 }
368
369 false
370}
371
372pub fn classify_kernel_timer(function: u64, kernel_start: u64, kernel_end: u64) -> bool {
381 if function == 0 {
382 return false;
383 }
384 !(function >= kernel_start && function <= kernel_end)
386}
387
388pub fn classify_notifier(notifier_call: u64, stext: u64, etext: u64) -> bool {
396 notifier_call < stext || notifier_call >= etext
397}
398
399const SUSPICIOUS_KMSG_PATTERNS: &[&str] = &[
405 "rootkit",
406 "hide",
407 "call trace",
408 "kernel bug",
409 "general protection",
410];
411
412pub fn classify_kmsg(text: &str) -> bool {
414 let lower = text.to_lowercase();
415 SUSPICIOUS_KMSG_PATTERNS.iter().any(|p| lower.contains(p))
416}
417
418const KERNEL_SPACE_MIN: u64 = 0xFFFF_0000_0000_0000;
424
425fn looks_like_hex_name(name: &str) -> bool {
429 let mut run = 0u32;
430 for ch in name.chars() {
431 if ch.is_ascii_hexdigit() {
432 run += 1;
433 if run >= 8 {
434 return true;
435 }
436 } else {
437 run = 0;
438 }
439 }
440 false
441}
442
443pub fn classify_kthread(name: &str, start_fn_addr: u64) -> (bool, Option<String>) {
448 if name.is_empty() {
450 return (true, Some("unnamed kernel thread".into()));
451 }
452
453 if KERNEL_THREAD_COMMS.iter().any(|p| name.starts_with(p)) {
455 return (false, None);
456 }
457
458 if start_fn_addr != 0 && start_fn_addr < KERNEL_SPACE_MIN {
460 return (
461 true,
462 Some(format!(
463 "thread function at userspace address {start_fn_addr:#x}"
464 )),
465 );
466 }
467
468 if looks_like_hex_name(name) {
470 return (
471 true,
472 Some(format!("name '{name}' contains suspicious hex pattern")),
473 );
474 }
475
476 (false, None)
477}
478
479fn parse_ld_preload(value: &str) -> Vec<String> {
485 value
486 .split(|c: char| c == ':' || c.is_ascii_whitespace())
487 .filter(|s| !s.is_empty())
488 .map(String::from)
489 .collect()
490}
491
492fn is_suspicious_ld_path(path: &str, safe_prefixes: &[&str]) -> bool {
494 if path.starts_with("/tmp/") || path == "/tmp" {
495 return true;
496 }
497 if path.starts_with("/dev/shm/") || path == "/dev/shm" {
498 return true;
499 }
500 if path
501 .split('/')
502 .any(|component| !component.is_empty() && component.starts_with('.'))
503 {
504 return true;
505 }
506 if !safe_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
507 return true;
508 }
509 false
510}
511
512pub fn classify_ld_preload(value: &str) -> bool {
517 const SAFE_PREFIXES: &[&str] = &[
518 "/usr/lib/",
519 "/usr/lib64/",
520 "/usr/lib32/",
521 "/usr/local/lib/",
522 "/usr/local/lib64/",
523 "/lib/",
524 "/lib64/",
525 "/lib32/",
526 ];
527
528 let libraries = parse_ld_preload(value);
529 libraries
530 .iter()
531 .any(|lib| is_suspicious_ld_path(lib, SAFE_PREFIXES))
532}
533
534pub fn classify_library(lib_path: &str) -> bool {
543 let path = lib_path.trim();
544
545 if path.ends_with("(deleted)") {
547 return true;
548 }
549
550 let clean = path.strip_suffix(" (deleted)").unwrap_or(path);
552
553 if clean.starts_with("/tmp/")
555 || clean == "/tmp"
556 || clean.starts_with("/dev/shm/")
557 || clean == "/dev/shm"
558 || clean.starts_with("/var/tmp/")
559 || clean == "/var/tmp"
560 {
561 return true;
562 }
563
564 if let Some(basename) = clean.rsplit('/').next() {
566 if basename.starts_with('.') && !basename.is_empty() {
567 return true;
568 }
569 }
570
571 if !std::path::Path::new(clean)
573 .extension()
574 .is_some_and(|e| e.eq_ignore_ascii_case("so"))
575 && !clean.contains(".so.")
576 {
577 return true;
578 }
579
580 false
581}
582
583const BENIGN_MEMFD_PREFIXES: &[&str] = &[
589 "shm",
590 "pulseaudio",
591 "wayland",
592 "dbus",
593 "chrome",
594 "firefox",
595 "v8",
596];
597
598const SUSPICIOUS_MEMFD_NAMES: &[&str] =
600 &["payload", "shellcode", "stage", "loader", "inject", "hack"];
601
602pub fn classify_memfd(name: &str, is_executable: bool) -> bool {
607 if is_executable {
609 return true;
610 }
611
612 let name_lower = name.to_lowercase();
613
614 for prefix in BENIGN_MEMFD_PREFIXES {
616 if name_lower.starts_with(prefix) {
617 return false;
618 }
619 }
620
621 if name.is_empty() {
623 return true;
624 }
625
626 for s in SUSPICIOUS_MEMFD_NAMES {
628 if name_lower.contains(s) {
629 return true;
630 }
631 }
632
633 false
634}
635
636pub fn classify_module_visibility(
645 in_module_list: bool,
646 in_kobj_list: bool,
647 in_memory_map: bool,
648) -> bool {
649 let present_count = [in_module_list, in_kobj_list, in_memory_map]
650 .iter()
651 .filter(|&&v| v)
652 .count();
653
654 present_count > 0 && present_count < 3
656}
657
658pub fn classify_mount(fs_type: &str, dev_name: &str, mnt_root: &str) -> bool {
667 let _ = dev_name;
668 match fs_type {
669 "tmpfs" | "ramfs" => {
670 !matches!(
671 mnt_root,
672 "/tmp" | "/run" | "/dev/shm" | "/run/lock" | "/run/user" | "/"
673 ) && !mnt_root.starts_with("/run/")
674 && !mnt_root.starts_with("/tmp/")
675 && !mnt_root.starts_with("/dev/")
676 }
677 "overlay" | "overlayfs" => {
678 !mnt_root.starts_with("/var/lib/docker") && !mnt_root.starts_with("/var/lib/containerd")
679 }
680 _ => false,
681 }
682}
683
684const SUSPICIOUS_OOM_NAMES: &[&str] = &[
690 "auditd",
691 "sshd",
692 "systemd",
693 "journald",
694 "rsyslogd",
695 "containerd",
696 "dockerd",
697];
698
699pub fn classify_oom_victim(comm: &str, pid: u32) -> bool {
704 let lower = comm.to_ascii_lowercase();
705 SUSPICIOUS_OOM_NAMES.iter().any(|n| lower.contains(n)) || pid < 100
706}
707
708const SYSTEM_LIB_PREFIXES: &[&str] =
714 &["/lib", "/usr/lib", "/usr/lib64", "/lib64", "/usr/local/lib"];
715
716pub fn classify_pam_hook(path: &str) -> bool {
721 if path.is_empty() {
722 return false;
723 }
724 let lower = path.to_lowercase();
725 if !lower.contains("pam") {
726 return false;
727 }
728 !SYSTEM_LIB_PREFIXES
729 .iter()
730 .any(|prefix| path.starts_with(prefix))
731}
732
733pub fn classify_perf_event(event_type: u32, config: u64) -> bool {
741 match event_type {
742 3 => (config & 0xFF) <= 2, 4 => true, _ => false,
745 }
746}
747
748const PF_KTHREAD: u64 = 0x0020_0000;
754const VSIZE_ABUSE_THRESHOLD: u64 = 100 * 1024 * 1024 * 1024;
756
757pub fn classify_psaux(state: u64, uid: u32, flags: u64, vsize: u64) -> bool {
762 if state == 16 && uid == 0 {
763 return true;
764 }
765 if (flags & PF_KTHREAD) != 0 && uid != 0 {
766 return true;
767 }
768 if vsize > VSIZE_ABUSE_THRESHOLD {
769 return true;
770 }
771 false
772}
773
774const KNOWN_DEBUGGERS: &[&str] = &["gdb", "lldb", "strace", "ltrace", "valgrind", "perf"];
780
781const HIGH_VALUE_TARGETS: &[&str] = &["sshd", "login", "passwd", "sudo", "su", "gpg-agent"];
783
784pub fn classify_ptrace(tracer_name: &str, tracee_name: &str) -> bool {
789 if tracer_name.is_empty() {
790 return true;
791 }
792 if KNOWN_DEBUGGERS.contains(&tracer_name) {
793 return false;
794 }
795 if HIGH_VALUE_TARGETS.contains(&tracee_name) {
796 return true;
797 }
798 if tracer_name == tracee_name {
799 return true;
800 }
801 false
802}
803
804const BENIGN_AF_PACKET: &[&str] = &[
810 "tcpdump",
811 "wireshark",
812 "dumpcap",
813 "dhclient",
814 "dhcpcd",
815 "arping",
816 "ping",
817 "ping6",
818];
819
820const BENIGN_SOCK_RAW: &[&str] = &["ping", "ping6", "traceroute", "traceroute6", "arping"];
822
823pub fn classify_raw_socket(comm: &str, socket_type: &str, is_promiscuous: bool) -> bool {
828 if is_promiscuous {
829 return true;
830 }
831
832 let comm_lower = comm.to_lowercase();
833
834 if socket_type == "AF_PACKET" {
835 return !BENIGN_AF_PACKET.iter().any(|&b| comm_lower == b);
836 }
837
838 if socket_type == "SOCK_RAW" {
839 return !BENIGN_SOCK_RAW.iter().any(|&b| comm_lower == b);
840 }
841
842 false
843}
844
845pub fn classify_signal_handler(signal: u32, handler: u64) -> bool {
854 match signal {
855 15 | 1 => handler == 1,
857 11 => handler != 0 && handler != 1,
859 9 => handler != 0,
861 _ => false,
862 }
863}
864
865const SUSPICIOUS_EXEC_PATTERNS: &[&str] = &[
871 "/tmp/",
872 "/dev/shm/",
873 "/var/tmp/",
874 "curl",
875 "wget",
876 "bash -c",
877 "sh -c",
878 "python",
879 "perl",
880 "ruby",
881 "nc ",
882 "ncat",
883 "base64",
884];
885
886const SAFE_EXEC_PREFIXES: &[&str] = &["/usr/", "/bin/", "/sbin/", "/lib/"];
888
889const KNOWN_SAFE_UNITS: &[&str] = &["systemd-", "NetworkManager", "dbus", "cron", "ssh"];
891
892const UNIT_EXTENSIONS: &[&str] = &[".service", ".timer", ".socket", ".path", ".mount"];
894
895pub fn classify_systemd_unit(unit_name: &str, exec_start: &str) -> bool {
899 if KNOWN_SAFE_UNITS
901 .iter()
902 .any(|prefix| unit_name.starts_with(prefix))
903 {
904 return false;
905 }
906
907 if SAFE_EXEC_PREFIXES
909 .iter()
910 .any(|prefix| exec_start.starts_with(prefix))
911 {
912 return false;
913 }
914
915 if SUSPICIOUS_EXEC_PATTERNS
917 .iter()
918 .any(|pat| exec_start.contains(pat))
919 {
920 return true;
921 }
922
923 let stem = UNIT_EXTENSIONS
925 .iter()
926 .find_map(|ext| unit_name.strip_suffix(ext))
927 .unwrap_or(unit_name);
928 if stem.len() >= 8
929 && stem
930 .chars()
931 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
932 {
933 return true;
934 }
935
936 false
937}
938
939pub fn classify_tmpfs_file(filename: &str, mode: u32) -> bool {
947 let is_regular_file = (mode & 0o170_000) == 0o100_000;
949 let is_exec = is_regular_file && (mode & 0o111) != 0;
950 let is_hidden = filename.starts_with('.') && filename.len() > 1;
951 is_exec || is_hidden
952}
953
954pub fn classify_unix_socket(path: &str, owner_pid: u32) -> bool {
963 let is_abstract = path.is_empty() || path.starts_with('@');
964 if is_abstract && owner_pid >= 1000 {
965 return true;
966 }
967 if path.starts_with("/tmp") || path.starts_with("/dev/shm") {
968 return true;
969 }
970 false
971}
972
973const SUSPICIOUS_DAEMON_NAMES: &[&str] = &[
979 "sshd",
980 "httpd",
981 "nginx",
982 "apache",
983 "mysqld",
984 "postgres",
985 "redis",
986 "memcached",
987 "mongod",
988 "named",
989 "bind",
990 "cupsd",
991 "cron",
992 "atd",
993];
994
995pub fn classify_zombie_orphan(is_zombie: bool, is_orphan: bool, ppid: u32, comm: &str) -> bool {
1000 if is_zombie && ppid == 1 {
1001 return true;
1002 }
1003 if is_orphan {
1004 let lower = comm.to_lowercase();
1005 if SUSPICIOUS_DAEMON_NAMES
1006 .iter()
1007 .any(|&name| lower.contains(name))
1008 {
1009 return true;
1010 }
1011 }
1012 false
1013}
1014
1015#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1026 fn heuristics_bpf_kprobe_is_suspicious() {
1027 assert!(classify_bpf_program("kprobe", "my_hook"));
1028 }
1029
1030 #[test]
1031 fn heuristics_bpf_lsm_is_suspicious() {
1032 assert!(classify_bpf_program("lsm", ""));
1033 }
1034
1035 #[test]
1036 fn heuristics_bpf_xdp_benign() {
1037 assert!(!classify_bpf_program("xdp", "firewall"));
1038 }
1039
1040 #[test]
1041 fn heuristics_bpf_unnamed_tracing_suspicious() {
1042 assert!(classify_bpf_program("tracing", ""));
1043 }
1044
1045 #[test]
1046 fn heuristics_bpf_named_tracing_benign() {
1047 assert!(!classify_bpf_program("tracing", "named_prog"));
1048 }
1049
1050 #[test]
1053 fn heuristics_capabilities_root_never_suspicious() {
1054 let (susp, names) = classify_capabilities(u64::MAX, 0);
1055 assert!(!susp);
1056 assert!(names.is_empty());
1057 }
1058
1059 #[test]
1060 fn heuristics_capabilities_non_root_sys_admin_suspicious() {
1061 let cap_sys_admin: u64 = 1 << 21;
1062 let (susp, names) = classify_capabilities(cap_sys_admin, 1000);
1063 assert!(susp);
1064 assert!(!names.is_empty());
1065 }
1066
1067 #[test]
1068 fn heuristics_capabilities_non_root_no_caps_benign() {
1069 let (susp, names) = classify_capabilities(0, 1000);
1070 assert!(!susp);
1071 assert!(names.is_empty());
1072 }
1073
1074 #[test]
1077 fn heuristics_cgroup_docker_detected() {
1078 let (in_container, id) = classify_cgroup("/docker/abc123def456");
1079 assert!(in_container);
1080 assert_eq!(id, "abc123def456");
1081 }
1082
1083 #[test]
1084 fn heuristics_cgroup_bare_root_not_container() {
1085 let (in_container, id) = classify_cgroup("/");
1086 assert!(!in_container);
1087 assert!(id.is_empty());
1088 }
1089
1090 #[test]
1093 fn heuristics_afinfo_null_not_hooked() {
1094 assert!(!classify_afinfo_hook(0, 0xffff0000, 0xffff8000));
1095 }
1096
1097 #[test]
1098 fn heuristics_afinfo_in_range_benign() {
1099 assert!(!classify_afinfo_hook(0xffff1000, 0xffff0000, 0xffff8000));
1100 }
1101
1102 #[test]
1103 fn heuristics_afinfo_outside_range_suspicious() {
1104 assert!(classify_afinfo_hook(
1105 0x0000_dead_beef,
1106 0xffff0000,
1107 0xffff8000
1108 ));
1109 }
1110
1111 #[test]
1114 fn heuristics_shared_creds_userspace_shares_with_init_suspicious() {
1115 assert!(classify_shared_creds(500, &[1], 1000));
1116 }
1117
1118 #[test]
1119 fn heuristics_shared_creds_empty_list_benign() {
1120 assert!(!classify_shared_creds(500, &[], 1000));
1121 }
1122
1123 #[test]
1124 fn heuristics_shared_creds_kernel_thread_shares_with_init_benign() {
1125 assert!(!classify_shared_creds(2, &[1], 0));
1127 }
1128
1129 #[test]
1132 fn heuristics_idt_null_not_hooked() {
1133 assert!(!classify_idt_entry(0, 0xffff0000, 0xffff8000));
1134 }
1135
1136 #[test]
1137 fn heuristics_idt_in_kernel_range_benign() {
1138 assert!(!classify_idt_entry(0xffff2000, 0xffff0000, 0xffff8000));
1139 }
1140
1141 #[test]
1142 fn heuristics_idt_outside_range_suspicious() {
1143 assert!(classify_idt_entry(0x1234, 0xffff0000, 0xffff8000));
1144 }
1145
1146 #[test]
1149 fn heuristics_container_escape_namespace_mismatch_suspicious() {
1150 assert!(classify_container_escape("bash", "namespace_mismatch"));
1151 }
1152
1153 #[test]
1154 fn heuristics_container_escape_kernel_thread_benign() {
1155 assert!(!classify_container_escape(
1156 "kworker/0:0",
1157 "namespace_mismatch"
1158 ));
1159 }
1160
1161 #[test]
1162 fn heuristics_container_escape_unknown_indicator_benign() {
1163 assert!(!classify_container_escape("bash", "some_other_thing"));
1164 }
1165
1166 #[test]
1169 fn heuristics_deleted_exe_not_deleted_benign() {
1170 assert!(!classify_deleted_exe("/usr/bin/bash", "bash"));
1171 }
1172
1173 #[test]
1174 fn heuristics_deleted_exe_suspicious() {
1175 assert!(classify_deleted_exe("/tmp/evil (deleted)", "evil"));
1176 }
1177
1178 #[test]
1179 fn heuristics_deleted_exe_empty_comm_benign() {
1180 assert!(!classify_deleted_exe("/tmp/x (deleted)", ""));
1181 }
1182
1183 #[test]
1186 fn heuristics_hidden_dentry_nlink_zero_suspicious() {
1187 assert!(classify_hidden_dentry(0, "normal.txt"));
1188 }
1189
1190 #[test]
1191 fn heuristics_hidden_dentry_empty_filename_benign() {
1192 assert!(!classify_hidden_dentry(0, ""));
1193 }
1194
1195 #[test]
1196 fn heuristics_hidden_dentry_linked_no_suspicious_ext_benign() {
1197 assert!(!classify_hidden_dentry(1, "readme.txt"));
1198 }
1199
1200 #[test]
1203 fn heuristics_ebpf_map_ringbuf_suspicious() {
1204 assert!(classify_ebpf_map(26, "benign_name", 8));
1206 }
1207
1208 #[test]
1209 fn heuristics_ebpf_map_perf_event_array_suspicious() {
1210 assert!(classify_ebpf_map(3, "benign_name", 8));
1211 }
1212
1213 #[test]
1214 fn heuristics_ebpf_map_hash_benign_name_benign() {
1215 assert!(!classify_ebpf_map(1, "counters", 8));
1217 }
1218
1219 #[test]
1222 fn heuristics_ftrace_in_text_benign() {
1223 assert!(!classify_ftrace_hook(0x1000, 0x1000, 0x2000));
1224 }
1225
1226 #[test]
1227 fn heuristics_ftrace_outside_text_suspicious() {
1228 assert!(classify_ftrace_hook(0x500, 0x1000, 0x2000));
1229 }
1230
1231 #[test]
1234 fn heuristics_futex_high_waiter_count_suspicious() {
1235 assert!(classify_futex(0x1000, 0, 1001));
1236 }
1237
1238 #[test]
1239 fn heuristics_futex_normal_benign() {
1240 assert!(!classify_futex(0x1000, 0, 5));
1241 }
1242
1243 #[test]
1244 fn heuristics_futex_kernel_key_userspace_owner_suspicious() {
1245 assert!(classify_futex(0xffff_0000_0000, 1234, 0));
1246 }
1247
1248 #[test]
1251 fn heuristics_io_uring_no_seccomp_benign() {
1252 assert!(!classify_io_uring(&[1, 2, 3], 0));
1253 }
1254
1255 #[test]
1256 fn heuristics_io_uring_no_opcodes_benign() {
1257 assert!(!classify_io_uring(&[], 1));
1258 }
1259
1260 #[test]
1263 fn heuristics_iomem_kernel_code_name_benign() {
1264 assert!(!classify_iomem(
1265 "Kernel code",
1266 0xffff_ffff_8100_0000,
1267 0xffff_ffff_8180_0000
1268 ));
1269 }
1270
1271 #[test]
1272 fn heuristics_iomem_empty_name_small_region_benign() {
1273 assert!(!classify_iomem("", 0, 1024));
1275 }
1276
1277 #[test]
1278 fn heuristics_iomem_empty_name_large_region_suspicious() {
1279 assert!(classify_iomem("", 0, 2 * 1024 * 1024));
1280 }
1281
1282 #[test]
1285 fn heuristics_kernel_timer_null_benign() {
1286 assert!(!classify_kernel_timer(0, 0xffff0000, 0xffff8000));
1287 }
1288
1289 #[test]
1290 fn heuristics_kernel_timer_in_range_benign() {
1291 assert!(!classify_kernel_timer(0xffff1000, 0xffff0000, 0xffff8000));
1292 }
1293
1294 #[test]
1295 fn heuristics_kernel_timer_outside_range_suspicious() {
1296 assert!(classify_kernel_timer(0x1234, 0xffff0000, 0xffff8000));
1297 }
1298
1299 #[test]
1302 fn heuristics_notifier_in_text_benign() {
1303 assert!(!classify_notifier(0x1000, 0x1000, 0x2000));
1304 }
1305
1306 #[test]
1307 fn heuristics_notifier_below_stext_suspicious() {
1308 assert!(classify_notifier(0x500, 0x1000, 0x2000));
1309 }
1310
1311 #[test]
1314 fn heuristics_kmsg_normal_message_benign() {
1315 assert!(!classify_kmsg("USB device connected"));
1316 }
1317
1318 #[test]
1321 fn heuristics_kthread_empty_name_suspicious() {
1322 let (susp, reason) = classify_kthread("", 0xffff_8000_0000);
1323 assert!(susp);
1324 assert!(reason.is_some());
1325 }
1326
1327 #[test]
1328 fn heuristics_kthread_named_kernel_fn_benign() {
1329 let (susp, _) = classify_kthread("kworker/0:0", 0xffff_8000_1234);
1330 assert!(!susp);
1331 }
1332
1333 #[test]
1336 fn heuristics_ld_preload_tmp_path_suspicious() {
1337 assert!(classify_ld_preload("/tmp/evil.so"));
1338 }
1339
1340 #[test]
1341 fn heuristics_ld_preload_system_lib_benign() {
1342 assert!(!classify_ld_preload("/usr/lib/libfoo.so"));
1343 }
1344
1345 #[test]
1348 fn heuristics_library_deleted_suspicious() {
1349 assert!(classify_library("/usr/lib/libfoo.so (deleted)"));
1350 }
1351
1352 #[test]
1353 fn heuristics_library_normal_benign() {
1354 assert!(!classify_library("/usr/lib/libc.so.6"));
1355 }
1356
1357 #[test]
1358 fn heuristics_library_tmp_suspicious() {
1359 assert!(classify_library("/tmp/inject.so"));
1360 }
1361
1362 #[test]
1365 fn heuristics_memfd_executable_suspicious() {
1366 assert!(classify_memfd("legit_name", true));
1367 }
1368
1369 #[test]
1370 fn heuristics_memfd_empty_name_suspicious() {
1371 assert!(classify_memfd("", false));
1372 }
1373
1374 #[test]
1377 fn heuristics_module_visibility_all_present_benign() {
1378 assert!(!classify_module_visibility(true, true, true));
1379 }
1380
1381 #[test]
1382 fn heuristics_module_visibility_partial_hidden() {
1383 assert!(classify_module_visibility(true, false, true));
1384 }
1385
1386 #[test]
1387 fn heuristics_module_visibility_all_absent_benign() {
1388 assert!(!classify_module_visibility(false, false, false));
1390 }
1391
1392 #[test]
1395 fn heuristics_mount_known_tmpfs_root_benign() {
1396 assert!(!classify_mount("tmpfs", "tmpfs", "/tmp"));
1397 }
1398
1399 #[test]
1400 fn heuristics_mount_unknown_tmpfs_suspicious() {
1401 assert!(classify_mount("tmpfs", "tmpfs", "/secret_staging"));
1402 }
1403
1404 #[test]
1405 fn heuristics_mount_ext4_benign() {
1406 assert!(!classify_mount("ext4", "/dev/sda1", "/"));
1407 }
1408
1409 #[test]
1412 fn heuristics_oom_victim_low_pid_suspicious() {
1413 assert!(classify_oom_victim("bash", 5));
1414 }
1415
1416 #[test]
1417 fn heuristics_oom_victim_normal_benign() {
1418 assert!(!classify_oom_victim("chrome", 5000));
1419 }
1420
1421 #[test]
1424 fn heuristics_pam_hook_empty_benign() {
1425 assert!(!classify_pam_hook(""));
1426 }
1427
1428 #[test]
1429 fn heuristics_pam_hook_system_lib_benign() {
1430 assert!(!classify_pam_hook("/lib/x86_64-linux-gnu/libpam.so.0"));
1431 }
1432
1433 #[test]
1434 fn heuristics_pam_hook_tmp_suspicious() {
1435 assert!(classify_pam_hook("/tmp/fakepam.so"));
1436 }
1437
1438 #[test]
1441 fn heuristics_perf_event_raw_pmu_suspicious() {
1442 assert!(classify_perf_event(4, 0));
1443 }
1444
1445 #[test]
1446 fn heuristics_perf_event_software_benign() {
1447 assert!(!classify_perf_event(1, 0));
1448 }
1449
1450 #[test]
1453 fn heuristics_psaux_zombie_root_suspicious() {
1454 assert!(classify_psaux(16, 0, 0, 0));
1456 }
1457
1458 #[test]
1459 fn heuristics_psaux_normal_process_benign() {
1460 assert!(!classify_psaux(1, 1000, 0, 4096));
1461 }
1462
1463 #[test]
1466 fn heuristics_ptrace_empty_tracer_suspicious() {
1467 assert!(classify_ptrace("", "bash"));
1468 }
1469
1470 #[test]
1471 fn heuristics_ptrace_gdb_tracing_bash_benign() {
1472 assert!(!classify_ptrace("gdb", "bash"));
1473 }
1474
1475 #[test]
1478 fn heuristics_raw_socket_promiscuous_suspicious() {
1479 assert!(classify_raw_socket("tcpdump", "AF_PACKET", true));
1480 }
1481
1482 #[test]
1483 fn heuristics_raw_socket_tcpdump_not_promiscuous_benign() {
1484 assert!(!classify_raw_socket("tcpdump", "AF_PACKET", false));
1485 }
1486
1487 #[test]
1490 fn heuristics_signal_handler_sigterm_ignored_suspicious() {
1491 assert!(classify_signal_handler(15, 1));
1493 }
1494
1495 #[test]
1496 fn heuristics_signal_handler_sigterm_default_benign() {
1497 assert!(!classify_signal_handler(15, 0));
1498 }
1499
1500 #[test]
1501 fn heuristics_signal_handler_sigkill_nonzero_suspicious() {
1502 assert!(classify_signal_handler(9, 0x1234));
1503 }
1504
1505 #[test]
1508 fn heuristics_systemd_unit_suspicious_exec_start() {
1509 assert!(classify_systemd_unit("evil.service", "/tmp/backdoor.sh"));
1510 }
1511
1512 #[test]
1515 fn heuristics_tmpfs_file_executable_regular_suspicious() {
1516 assert!(classify_tmpfs_file("payload", 0o100_755));
1518 }
1519
1520 #[test]
1521 fn heuristics_tmpfs_file_hidden_suspicious() {
1522 assert!(classify_tmpfs_file(".hidden", 0o100_644));
1523 }
1524
1525 #[test]
1526 fn heuristics_tmpfs_file_normal_benign() {
1527 assert!(!classify_tmpfs_file("readme.txt", 0o100_644));
1528 }
1529
1530 #[test]
1533 fn heuristics_unix_socket_abstract_high_uid_suspicious() {
1534 assert!(classify_unix_socket("", 1234));
1536 }
1537
1538 #[test]
1539 fn heuristics_unix_socket_system_path_benign() {
1540 assert!(!classify_unix_socket("/var/run/docker.sock", 500));
1541 }
1542
1543 #[test]
1544 fn heuristics_unix_socket_tmp_suspicious() {
1545 assert!(classify_unix_socket("/tmp/evil.sock", 500));
1546 }
1547
1548 #[test]
1551 fn heuristics_zombie_orphan_reparented_to_init_suspicious() {
1552 assert!(classify_zombie_orphan(true, false, 1, "bash"));
1553 }
1554
1555 #[test]
1556 fn heuristics_zombie_orphan_normal_benign() {
1557 assert!(!classify_zombie_orphan(false, false, 1234, "chrome"));
1558 }
1559}