pub mod entropy;
pub mod evtx;
pub mod linux_rootkit;
pub mod memory;
pub mod network;
pub mod paths;
pub mod pe;
pub mod process;
pub mod ransomware;
pub mod scoring;
pub mod srum;
pub mod timestamps;
pub const BORN_BEFORE_INSTALL_THRESHOLD_NS: i64 = 86_400_000_000_000;
pub const FILETIME_EPOCH_DIFF_100NS: i128 = 116_444_736_000_000_000;
pub const WEBKIT_EPOCH_OFFSET_US: i64 = 11_644_473_600 * 1_000_000;
pub const CORE_DATA_EPOCH_OFFSET_SECS: i64 = 978_307_200;
pub const WORKING_HOURS_START: u32 = 9;
pub const WORKING_HOURS_END: u32 = 17;
#[must_use]
pub fn is_born_before_install(file_born_ns: i64, os_install_ns: i64) -> bool {
file_born_ns < os_install_ns - BORN_BEFORE_INSTALL_THRESHOLD_NS
}
#[must_use]
pub fn is_hour_outside_working_hours(hour: u8) -> bool {
let h = u32::from(hour);
!(WORKING_HOURS_START..WORKING_HOURS_END).contains(&h)
}
pub const SI_PRECEDES_FN_THRESHOLD_NS: i64 = 1_000_000_000;
#[must_use]
pub fn is_si_before_fn(si_born_ns: i64, fn_born_ns: i64) -> bool {
fn_born_ns - si_born_ns > SI_PRECEDES_FN_THRESHOLD_NS
}
pub const NULL_TIMESTAMP_WINDOW_NS: i64 = 86_400_000_000_000;
#[must_use]
pub fn is_null_timestamp(ts_ns: i64) -> bool {
ts_ns < NULL_TIMESTAMP_WINDOW_NS
}
pub const RAPID_ACCESS_THRESHOLD_NS: i64 = 1_000_000_000;
#[must_use]
pub fn is_rapid_sequence(ts1_ns: i64, ts2_ns: i64) -> bool {
(ts1_ns - ts2_ns).unsigned_abs() < RAPID_ACCESS_THRESHOLD_NS as u64
}
#[must_use]
pub fn is_burst_access(timestamps_ns: &[i64]) -> bool {
if timestamps_ns.len() < 2 {
return false;
}
timestamps_ns
.windows(2)
.all(|w| is_rapid_sequence(w[0], w[1]))
}
pub const MIN_EPHEMERAL_PORT: u16 = 49152;
pub const MAX_REGISTERED_PORT: u16 = 49151;
pub const MIN_RESERVED_PORT: u16 = 1;
pub const MAX_RESERVED_PORT: u16 = 1023;
pub const MINER_STRATUM_PORTS: &[u16] = &[3333, 4444, 5555, 14444, 45700];
pub const COMMON_TUNNEL_PORTS: &[u16] = &[1080, 3128, 8080, 8443, 9050, 9150];
#[must_use]
pub fn is_ephemeral_port(port: u16) -> bool {
port >= MIN_EPHEMERAL_PORT
}
#[must_use]
pub fn is_miner_port(port: u16) -> bool {
MINER_STRATUM_PORTS.contains(&port)
}
#[must_use]
pub fn is_common_tunnel_port(port: u16) -> bool {
COMMON_TUNNEL_PORTS.contains(&port)
}
pub const MAX_SYSTEM_UID: u32 = 999;
#[must_use]
pub fn is_system_uid(uid: u32) -> bool {
uid <= MAX_SYSTEM_UID
}
#[must_use]
pub fn is_user_uid(uid: u32) -> bool {
uid > MAX_SYSTEM_UID
}
pub const BEACON_JITTER_PPT: u64 = 25;
#[must_use]
pub fn is_regular_interval(intervals_ns: &[i64]) -> bool {
if intervals_ns.len() < 3 {
return false;
}
if intervals_ns.iter().any(|&v| v <= 0) {
return false;
}
let mut sorted: [i64; 64] = [0; 64];
let n = intervals_ns.len().min(sorted.len());
sorted[..n].copy_from_slice(&intervals_ns[..n]);
for i in 1..n {
let key = sorted[i];
let mut j = i;
while j > 0 && sorted[j - 1] > key {
sorted[j] = sorted[j - 1];
j -= 1;
}
sorted[j] = key;
}
let median = if n % 2 == 1 {
sorted[n / 2]
} else {
(sorted[n / 2 - 1] / 2) + (sorted[n / 2] / 2)
};
let tolerance = (median as u64).saturating_mul(BEACON_JITTER_PPT) / 1000;
let lo = median - tolerance as i64;
let hi = median + tolerance as i64;
intervals_ns[..n].iter().all(|&v| v >= lo && v <= hi)
}
pub const MIN_MEANINGFUL_FILE_BYTES: u64 = 4;
pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1_073_741_824;
#[must_use]
pub fn is_hollow_file(size_bytes: u64) -> bool {
size_bytes < MIN_MEANINGFUL_FILE_BYTES
}
#[must_use]
pub fn is_potential_container(size_bytes: u64) -> bool {
size_bytes >= LARGE_FILE_THRESHOLD_BYTES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn born_before_install_threshold_is_24h() {
assert_eq!(BORN_BEFORE_INSTALL_THRESHOLD_NS, 24 * 3_600_000_000_000i64);
}
#[test]
fn filetime_epoch_diff_is_correct() {
assert_eq!(FILETIME_EPOCH_DIFF_100NS, 11_644_473_600i128 * 10_000_000);
}
#[test]
fn working_hours_start_is_nine() {
assert_eq!(WORKING_HOURS_START, 9);
}
#[test]
fn working_hours_end_is_seventeen() {
assert_eq!(WORKING_HOURS_END, 17);
}
#[test]
fn born_two_days_before_install_returns_true() {
let install_ns = 1_700_000_000_000_000_000i64; let file_born_ns = install_ns - 2 * BORN_BEFORE_INSTALL_THRESHOLD_NS;
assert!(is_born_before_install(file_born_ns, install_ns));
}
#[test]
fn born_after_install_returns_false() {
let install_ns = 1_700_000_000_000_000_000i64;
let file_born_ns = install_ns + BORN_BEFORE_INSTALL_THRESHOLD_NS;
assert!(!is_born_before_install(file_born_ns, install_ns));
}
#[test]
fn born_twelve_hours_before_install_returns_false() {
let install_ns = 1_700_000_000_000_000_000i64;
let file_born_ns = install_ns - BORN_BEFORE_INSTALL_THRESHOLD_NS / 2;
assert!(!is_born_before_install(file_born_ns, install_ns));
}
#[test]
fn born_exactly_at_threshold_returns_false() {
let install_ns = 1_700_000_000_000_000_000i64;
let file_born_ns = install_ns - BORN_BEFORE_INSTALL_THRESHOLD_NS;
assert!(!is_born_before_install(file_born_ns, install_ns));
}
#[test]
fn hour_8_is_outside_working_hours() {
assert!(is_hour_outside_working_hours(8));
}
#[test]
fn hour_9_is_inside_working_hours() {
assert!(!is_hour_outside_working_hours(9));
}
#[test]
fn hour_16_is_inside_working_hours() {
assert!(!is_hour_outside_working_hours(16));
}
#[test]
fn hour_17_is_outside_working_hours() {
assert!(is_hour_outside_working_hours(17));
}
#[test]
fn hour_0_midnight_is_outside_working_hours() {
assert!(is_hour_outside_working_hours(0));
}
#[test]
fn hour_23_is_outside_working_hours() {
assert!(is_hour_outside_working_hours(23));
}
#[test]
fn si_before_fn_threshold_is_one_second() {
assert_eq!(SI_PRECEDES_FN_THRESHOLD_NS, 1_000_000_000i64);
}
#[test]
fn si_two_seconds_before_fn_is_timestomp() {
let fn_born = 1_700_000_000_000_000_000i64;
let si_born = fn_born - 2 * SI_PRECEDES_FN_THRESHOLD_NS;
assert!(is_si_before_fn(si_born, fn_born));
}
#[test]
fn si_after_fn_is_not_timestomp() {
let fn_born = 1_700_000_000_000_000_000i64;
let si_born = fn_born + SI_PRECEDES_FN_THRESHOLD_NS;
assert!(!is_si_before_fn(si_born, fn_born));
}
#[test]
fn si_equal_fn_is_not_timestomp() {
let ts = 1_700_000_000_000_000_000i64;
assert!(!is_si_before_fn(ts, ts));
}
#[test]
fn si_exactly_at_threshold_is_not_timestomp() {
let fn_born = 1_700_000_000_000_000_000i64;
let si_born = fn_born - SI_PRECEDES_FN_THRESHOLD_NS;
assert!(!is_si_before_fn(si_born, fn_born));
}
#[test]
fn null_timestamp_window_is_one_day() {
assert_eq!(NULL_TIMESTAMP_WINDOW_NS, 86_400_000_000_000i64);
}
#[test]
fn zero_timestamp_is_null() {
assert!(is_null_timestamp(0));
}
#[test]
fn negative_timestamp_is_null() {
assert!(is_null_timestamp(-1));
}
#[test]
fn timestamp_within_one_day_of_epoch_is_null() {
assert!(is_null_timestamp(NULL_TIMESTAMP_WINDOW_NS - 1));
}
#[test]
fn timestamp_exactly_at_one_day_is_not_null() {
assert!(!is_null_timestamp(NULL_TIMESTAMP_WINDOW_NS));
}
#[test]
fn modern_timestamp_is_not_null() {
assert!(!is_null_timestamp(1_704_067_200_000_000_000i64));
}
#[test]
fn rapid_access_threshold_is_one_second() {
assert_eq!(RAPID_ACCESS_THRESHOLD_NS, 1_000_000_000i64);
}
#[test]
fn two_events_half_second_apart_are_rapid() {
assert!(is_rapid_sequence(0, 500_000_000));
}
#[test]
fn two_events_two_seconds_apart_are_not_rapid() {
assert!(!is_rapid_sequence(0, 2_000_000_000));
}
#[test]
fn rapid_sequence_with_reversed_order() {
assert!(is_rapid_sequence(500_000_000, 0));
}
#[test]
fn burst_access_all_rapid_returns_true() {
let ts = [0i64, 100_000_000, 200_000_000, 300_000_000];
assert!(is_burst_access(&ts));
}
#[test]
fn burst_access_one_slow_gap_returns_false() {
let ts = [0i64, 100_000_000, 2_000_000_000, 2_100_000_000];
assert!(!is_burst_access(&ts));
}
#[test]
fn burst_access_empty_returns_false() {
assert!(!is_burst_access(&[]));
}
#[test]
fn burst_access_single_element_returns_false() {
assert!(!is_burst_access(&[42]));
}
#[test]
fn ephemeral_port_boundary_correct() {
assert_eq!(MIN_EPHEMERAL_PORT, 49152u16);
}
#[test]
fn port_49152_is_ephemeral() {
assert!(is_ephemeral_port(49152));
}
#[test]
fn port_49151_is_not_ephemeral() {
assert!(!is_ephemeral_port(49151));
}
#[test]
fn port_65535_is_ephemeral() {
assert!(is_ephemeral_port(65535));
}
#[test]
fn known_miner_port_3333_detected() {
assert!(is_miner_port(3333));
}
#[test]
fn known_miner_port_14444_detected() {
assert!(is_miner_port(14444));
}
#[test]
fn non_miner_port_80_not_detected() {
assert!(!is_miner_port(80));
}
#[test]
fn tunnel_port_9050_tor_detected() {
assert!(is_common_tunnel_port(9050));
}
#[test]
fn tunnel_port_1080_socks_detected() {
assert!(is_common_tunnel_port(1080));
}
#[test]
fn non_tunnel_port_443_not_detected() {
assert!(!is_common_tunnel_port(443));
}
#[test]
fn max_system_uid_is_999() {
assert_eq!(MAX_SYSTEM_UID, 999u32);
}
#[test]
fn root_uid_0_is_system() {
assert!(is_system_uid(0));
}
#[test]
fn uid_999_is_system() {
assert!(is_system_uid(999));
}
#[test]
fn uid_1000_is_user() {
assert!(is_user_uid(1000));
assert!(!is_system_uid(1000));
}
#[test]
fn uid_999_is_not_user() {
assert!(!is_user_uid(999));
}
#[test]
fn beacon_jitter_ppt_is_25() {
assert_eq!(BEACON_JITTER_PPT, 25u64);
}
#[test]
fn perfectly_regular_intervals_detected() {
let iv = [60_000_000_000i64; 5];
assert!(is_regular_interval(&iv));
}
#[test]
fn intervals_within_jitter_detected_as_regular() {
let base = 60_000_000_000i64;
let jitter = base / 50; let iv = [base - jitter, base, base + jitter, base, base - jitter / 2];
assert!(is_regular_interval(&iv));
}
#[test]
fn irregular_intervals_not_detected_as_beacon() {
let iv = [
60_000_000_000i64,
120_000_000_000,
30_000_000_000,
90_000_000_000,
60_000_000_000,
];
assert!(!is_regular_interval(&iv));
}
#[test]
fn fewer_than_three_intervals_returns_false() {
assert!(!is_regular_interval(&[60_000_000_000i64, 60_000_000_000]));
}
#[test]
fn empty_intervals_returns_false() {
assert!(!is_regular_interval(&[]));
}
#[test]
fn negative_interval_returns_false() {
assert!(!is_regular_interval(&[
60_000_000_000i64,
-1,
60_000_000_000
]));
}
#[test]
fn min_meaningful_file_bytes_is_4() {
assert_eq!(MIN_MEANINGFUL_FILE_BYTES, 4u64);
}
#[test]
fn large_file_threshold_is_1_gib() {
assert_eq!(LARGE_FILE_THRESHOLD_BYTES, 1_073_741_824u64);
}
#[test]
fn zero_byte_file_is_hollow() {
assert!(is_hollow_file(0));
}
#[test]
fn three_byte_file_is_hollow() {
assert!(is_hollow_file(3));
}
#[test]
fn four_byte_file_is_not_hollow() {
assert!(!is_hollow_file(4));
}
#[test]
fn one_gib_file_is_potential_container() {
assert!(is_potential_container(1_073_741_824));
}
#[test]
fn two_gib_file_is_potential_container() {
assert!(is_potential_container(2_147_483_648));
}
#[test]
fn small_file_is_not_potential_container() {
assert!(!is_potential_container(1_000_000));
}
}