From 43550c3e64016ab2448c690f4dd246799412a1c8 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 11 Feb 2026 09:45:47 -0800 Subject: [PATCH 01/24] X-Smart-Branch-Parent: main From 742009acce3317cbdc108ae92668f8a16165b65a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 5 Feb 2026 11:34:13 -0800 Subject: [PATCH 02/24] Added unit tests and parameterized integration tests --- fact/src/event/mod.rs | 151 ++++++++++++++++++++++++++ fact/src/event/process.rs | 219 ++++++++++++++++++++++++++++++++++++++ tests/test_file_open.py | 24 ++++- 3 files changed, 389 insertions(+), 5 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index e5695653..46283470 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -358,3 +358,154 @@ impl From for fact_api::FileOwnershipChange { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper function to convert a Rust string to a c_char array for testing + fn string_to_c_char_array(s: &str) -> [c_char; N] { + let mut array = [0 as c_char; N]; + let bytes = s.as_bytes(); + let len = bytes.len().min(N - 1); + for (i, &byte) in bytes.iter().take(len).enumerate() { + array[i] = byte as c_char; + } + array + } + + /// Helper function to convert raw bytes to a c_char array for testing invalid UTF-8 + fn bytes_to_c_char_array(bytes: &[u8]) -> [c_char; N] { + let mut array = [0 as c_char; N]; + let len = bytes.len().min(N - 1); + for (i, &byte) in bytes.iter().take(len).enumerate() { + array[i] = byte as c_char; + } + array + } + + #[test] + fn slice_to_string_valid_utf8() { + let tests = [ + ("hello", "ASCII"), + ("café", "Latin-1 supplement"), + ("файл", "Cyrillic"), + ("测试文件", "Chinese"), + ("test🚀file", "emoji"), + ("test-файл-测试-🐛.txt", "mixed characters"), + ("ملف", "Arabic"), + ("קובץ", "Hebrew"), + ("ファイル", "Japanese"), + ]; + + for (input, description) in tests { + let arr = string_to_c_char_array::<256>(input); + assert_eq!(slice_to_string(&arr).unwrap(), input, "Failed for {}", description); + } + } + + #[test] + fn slice_to_string_invalid_utf8() { + let tests: &[(&[u8], &str)] = &[ + (&[0xFF, 0xFE, 0xFD], "invalid continuation bytes"), + (&[b't', b'e', b's', b't', 0xE2], "truncated multi-byte sequence"), + (&[0xC0, 0x80], "overlong encoding"), + (&[b'h', b'e', b'l', b'l', b'o', 0x80, b'w', b'o', b'r', b'l', b'd'], "invalid start byte"), + (&[0x80], "lone continuation byte"), + (&[b't', b'e', b's', b't', 0xFF, 0xFE], "mixed valid and invalid bytes"), + ]; + + for (bytes, description) in tests { + let arr = bytes_to_c_char_array::<256>(bytes); + assert!(slice_to_string(&arr).is_err(), "Should fail for {}", description); + } + } + + #[test] + fn sanitize_d_path_valid_utf8() { + let tests = [ + ("/etc/test", "/etc/test", "ASCII"), + ("/tmp/файл.txt", "/tmp/файл.txt", "Cyrillic"), + ("/home/user/测试文件.log", "/home/user/测试文件.log", "Chinese"), + ("/data/🚀rocket.dat", "/data/🚀rocket.dat", "emoji"), + ("/var/log/app-данные-数据-🐛.log", "/var/log/app-данные-数据-🐛.log", "mixed Unicode"), + ("/home/ملف.txt", "/home/ملف.txt", "Arabic"), + ("/opt/ファイル.conf", "/opt/ファイル.conf", "Japanese"), + ]; + + for (input, expected, description) in tests { + let arr = string_to_c_char_array::<4096>(input); + assert_eq!(sanitize_d_path(&arr), PathBuf::from(expected), "Failed for {}", description); + } + } + + #[test] + fn sanitize_d_path_deleted_suffix() { + let tests = [ + ("/tmp/test.txt (deleted)", "/tmp/test.txt", "ASCII with deleted suffix"), + ("/tmp/файл.txt (deleted)", "/tmp/файл.txt", "Unicode with deleted suffix"), + ("/etc/config.yaml", "/etc/config.yaml", "no deleted suffix"), + ("/var/log/app/debug.log (deleted)", "/var/log/app/debug.log", "nested path with deleted suffix"), + ]; + + for (input, expected, description) in tests { + let arr = string_to_c_char_array::<4096>(input); + assert_eq!(sanitize_d_path(&arr), PathBuf::from(expected), "Failed for {}", description); + } + } + + #[test] + fn sanitize_d_path_invalid_utf8() { + let tests: &[(&[u8], &str, &str, &str)] = &[ + ( + &[b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b'.', b't', b'x', b't'], + "/tmp/", + ".txt", + "invalid continuation bytes", + ), + ( + &[b'/', b'v', b'a', b'r', b'/', b't', b'e', b's', b't', 0xE2, 0x80], + "/var/", + "", + "truncated multi-byte sequence", + ), + ( + &[b'/', b'h', b'o', b'm', b'e', b'/', b'f', b'i', b'l', b'e', 0x80, b'.', b'l', b'o', b'g'], + "/home/", + ".log", + "invalid start byte", + ), + ( + &[b'/', b't', b'm', b'p', b'/', 0xD1, 0x84, 0xFF, 0xD0, 0xBB, b'.', b't', b'x', b't'], + "/tmp/", + "", + "mixed valid and invalid UTF-8", + ), + ]; + + for (bytes, must_contain1, must_contain2, description) in tests { + let arr = bytes_to_c_char_array::<4096>(bytes); + let result = sanitize_d_path(&arr); + let result_str = result.to_string_lossy(); + + assert!(result_str.contains(must_contain1), "Failed for {} - should contain '{}'", description, must_contain1); + if !must_contain2.is_empty() { + assert!(result_str.contains(must_contain2), "Failed for {} - should contain '{}'", description, must_contain2); + } + assert!(result_str.contains('\u{FFFD}'), "Failed for {} - should contain replacement character", description); + } + } + + #[test] + fn sanitize_d_path_invalid_utf8_with_deleted_suffix() { + let invalid_with_deleted = bytes_to_c_char_array::<4096>(&[ + b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b' ', b'(', b'd', b'e', b'l', b'e', b't', b'e', b'd', b')', + ]); + let result = sanitize_d_path(&invalid_with_deleted); + let result_str = result.to_string_lossy(); + + assert!(result_str.contains("/tmp/")); + assert!(!result_str.ends_with(" (deleted)")); + assert!(result_str.contains('\u{FFFD}')); + } +} diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index d7d1d139..080239ab 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -222,6 +222,49 @@ impl From for fact_api::ProcessSignal { #[cfg(test)] mod tests { use super::*; + use std::os::raw::c_char; + + /// Helper function to convert a Rust string to a c_char array for testing + fn string_to_c_char_array(s: &str) -> [c_char; N] { + let mut array = [0 as c_char; N]; + let bytes = s.as_bytes(); + let len = bytes.len().min(N - 1); + for (i, &byte) in bytes.iter().take(len).enumerate() { + array[i] = byte as c_char; + } + array + } + + /// Helper function to convert raw bytes to a c_char array for testing invalid UTF-8 + fn bytes_to_c_char_array(bytes: &[u8]) -> [c_char; N] { + let mut array = [0 as c_char; N]; + let len = bytes.len().min(N - 1); + for (i, &byte) in bytes.iter().take(len).enumerate() { + array[i] = byte as c_char; + } + array + } + + /// Helper to create a default process_t for testing + fn default_process_t() -> process_t { + process_t { + comm: string_to_c_char_array::<16>("test"), + args: string_to_c_char_array::<4096>("arg1\0arg2\0"), + args_len: 10, + exe_path: string_to_c_char_array::<4096>("/usr/bin/test"), + memory_cgroup: string_to_c_char_array::<4096>("init.scope"), + uid: 1000, + gid: 1000, + login_uid: 1000, + pid: 12345, + lineage: [lineage_t { + uid: 1000, + exe_path: string_to_c_char_array::<4096>("/bin/bash"), + }; 2], + lineage_len: 0, + in_root_mount_ns: 1, + } + } #[test] fn extract_container_id() { @@ -259,4 +302,180 @@ mod tests { assert_eq!(id, expected); } } + + #[test] + fn process_conversion_valid_utf8_comm() { + let tests = [ + ("test", "ASCII"), + ("тест", "Cyrillic"), + ("测试", "Chinese"), + ("app🚀", "emoji"), + ]; + + for (comm, description) in tests { + let mut proc = default_process_t(); + proc.comm = string_to_c_char_array::<16>(comm); + let result = Process::try_from(proc); + assert!(result.is_ok(), "Failed for {}", description); + assert_eq!(result.unwrap().comm, comm, "Failed for {}", description); + } + } + + #[test] + fn process_conversion_invalid_utf8_comm() { + let tests: &[(&[u8], &str)] = &[ + (&[b't', b'e', b's', b't', 0xFF, 0xFE], "invalid bytes"), + (&[b'a', b'p', b'p', 0xE2, 0x80], "truncated multi-byte sequence"), + ]; + + for (bytes, description) in tests { + let mut proc = default_process_t(); + proc.comm = bytes_to_c_char_array::<16>(bytes); + let result = Process::try_from(proc); + assert!(result.is_err(), "Should fail for {}", description); + } + } + + #[test] + fn process_conversion_valid_utf8_exe_path() { + let tests = [ + ("/usr/bin/test", "ASCII"), + ("/usr/bin/тест", "Cyrillic"), + ("/opt/应用/测试", "Chinese"), + ("/home/user/🚀app", "emoji"), + ("/var/app-данные-数据/bin", "mixed UTF-8"), + ]; + + for (path, description) in tests { + let mut proc = default_process_t(); + proc.exe_path = string_to_c_char_array::<4096>(path); + let result = Process::try_from(proc); + assert!(result.is_ok(), "Failed for {}", description); + assert_eq!(result.unwrap().exe_path, PathBuf::from(path), "Failed for {}", description); + } + } + + #[test] + fn process_conversion_invalid_utf8_exe_path() { + let mut proc = default_process_t(); + proc.exe_path = bytes_to_c_char_array::<4096>(&[ + b'/', b'u', b's', b'r', b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE, + ]); + let result = Process::try_from(proc); + assert!(result.is_ok()); + let exe_path = result.unwrap().exe_path; + assert!(exe_path.to_string_lossy().contains("/usr/bin/")); + assert!(exe_path.to_string_lossy().contains('\u{FFFD}')); + } + + #[test] + fn process_conversion_valid_utf8_args() { + let tests: &[(&str, Vec<&str>, &str)] = &[ + ("arg1\0arg2\0arg3\0", vec!["arg1", "arg2", "arg3"], "ASCII"), + ("файл\0данные\0", vec!["файл", "данные"], "Cyrillic"), + ("测试\0文件\0数据\0", vec!["测试", "文件", "数据"], "Chinese"), + ("app\0🚀file\0📁data\0", vec!["app", "🚀file", "📁data"], "emoji"), + ("test\0файл\0测试\0🚀\0", vec!["test", "файл", "测试", "🚀"], "mixed UTF-8"), + ]; + + for (args_str, expected, description) in tests { + let mut proc = default_process_t(); + proc.args = string_to_c_char_array::<4096>(args_str); + proc.args_len = args_str.len() as u32; + let result = Process::try_from(proc); + assert!(result.is_ok(), "Failed for {}", description); + assert_eq!(result.unwrap().args, *expected, "Failed for {}", description); + } + } + + #[test] + fn process_conversion_invalid_utf8_args() { + let tests: &[(&[u8], u32, &str)] = &[ + (&[b'a', b'r', b'g', b'1', 0, 0xFF, 0xFE, b'a', b'r', b'g', 0], 11, "invalid bytes"), + (&[b't', b'e', b's', b't', 0, 0xE2, 0x80, 0], 8, "truncated multi-byte sequence"), + ]; + + for (bytes, args_len, description) in tests { + let mut proc = default_process_t(); + proc.args = bytes_to_c_char_array::<4096>(bytes); + proc.args_len = *args_len; + let result = Process::try_from(proc); + assert!(result.is_err(), "Should fail for {}", description); + } + } + + #[test] + fn process_conversion_valid_utf8_memory_cgroup() { + let tests = [ + ("init.scope", None, "ASCII init.scope"), + ( + "/docker/951e643e3c241b225b6284ef2b79a37c13fc64cbf65b5d46bda95fcb98fe63a4", + Some("951e643e3c24"), + "container ID", + ), + ]; + + for (cgroup, expected_id, description) in tests { + let mut proc = default_process_t(); + proc.memory_cgroup = string_to_c_char_array::<4096>(cgroup); + let result = Process::try_from(proc); + assert!(result.is_ok(), "Failed for {}", description); + assert_eq!( + result.unwrap().container_id, + expected_id.map(|s| s.to_string()), + "Failed for {}", + description + ); + } + } + + #[test] + fn process_conversion_invalid_utf8_memory_cgroup() { + let mut proc = default_process_t(); + proc.memory_cgroup = bytes_to_c_char_array::<4096>(&[ + b'/', b'd', b'o', b'c', b'k', b'e', b'r', b'/', 0xFF, 0xFE, + ]); + let result = Process::try_from(proc); + assert!(result.is_err()); + } + + #[test] + fn process_conversion_valid_utf8_lineage() { + let tests = [ + ("/bin/bash", "ASCII"), + ("/usr/bin/тест", "Cyrillic"), + ("/opt/应用", "Chinese"), + ]; + + for (path, description) in tests { + let mut proc = default_process_t(); + proc.lineage[0] = lineage_t { + uid: 1000, + exe_path: string_to_c_char_array::<4096>(path), + }; + proc.lineage_len = 1; + let result = Process::try_from(proc); + assert!(result.is_ok(), "Failed for {}", description); + let lineage = result.unwrap().lineage; + assert_eq!(lineage.len(), 1); + assert_eq!(lineage[0].exe_path, PathBuf::from(path), "Failed for {}", description); + } + } + + #[test] + fn process_conversion_invalid_utf8_lineage() { + let mut proc = default_process_t(); + proc.lineage[0] = lineage_t { + uid: 1000, + exe_path: bytes_to_c_char_array::<4096>(&[ + b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE, + ]), + }; + proc.lineage_len = 1; + let result = Process::try_from(proc); + assert!(result.is_ok()); + let lineage = result.unwrap().lineage; + assert!(lineage[0].exe_path.to_string_lossy().contains("/bin/")); + assert!(lineage[0].exe_path.to_string_lossy().contains('\u{FFFD}')); + } } diff --git a/tests/test_file_open.py b/tests/test_file_open.py index c47272c7..20ff4f4b 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -2,11 +2,19 @@ import os import docker +import pytest from event import Event, EventType, Process -def test_open(fact, monitored_dir, server): +@pytest.mark.parametrize("filename", [ + 'create.txt', + 'café.txt', + 'файл.txt', + '测试.txt', + '🚀rocket.txt', +]) +def test_open(fact, monitored_dir, server, filename): """ Tests the opening of a file and verifies that the corresponding event is captured by the server. @@ -15,9 +23,10 @@ def test_open(fact, monitored_dir, server): fact: Fixture for file activity (only required to be running). monitored_dir: Temporary directory path for creating the test file. server: The server instance to communicate with. + filename: Name of the file to create (includes UTF-8 test cases). """ # File Under Test - fut = os.path.join(monitored_dir, 'create.txt') + fut = os.path.join(monitored_dir, filename) with open(fut, 'w') as f: f.write('This is a test') @@ -28,7 +37,11 @@ def test_open(fact, monitored_dir, server): server.wait_events([e]) -def test_multiple(fact, monitored_dir, server): +@pytest.mark.parametrize("filenames", [ + ['0.txt', '1.txt', '2.txt'], + ['café.txt', 'файл.txt', '测试.txt'], +]) +def test_multiple(fact, monitored_dir, server, filenames): """ Tests the opening of multiple files and verifies that the corresponding events are captured by the server. @@ -37,12 +50,13 @@ def test_multiple(fact, monitored_dir, server): fact: Fixture for file activity (only required to be running). monitored_dir: Temporary directory path for creating the test file. server: The server instance to communicate with. + filenames: List of filenames to create (includes UTF-8 test cases). """ events = [] process = Process.from_proc() # File Under Test - for i in range(3): - fut = os.path.join(monitored_dir, f'{i}.txt') + for filename in filenames: + fut = os.path.join(monitored_dir, filename) with open(fut, 'w') as f: f.write('This is a test') From 1c8109656dda577cf0befa42a1730727fc18c674 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 5 Feb 2026 21:44:34 -0800 Subject: [PATCH 03/24] Reduced duplication of helper functions --- fact/src/event/mod.rs | 26 +++++++++++++------------- fact/src/event/process.rs | 22 +--------------------- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 46283470..d34b395b 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -360,13 +360,12 @@ impl From for fact_api::FileOwnershipChange { } #[cfg(test)] -mod tests { - use super::*; +mod test_utils { + use std::os::raw::c_char; - /// Helper function to convert a Rust string to a c_char array for testing - fn string_to_c_char_array(s: &str) -> [c_char; N] { + /// Helper function to convert raw bytes to a c_char array for testing + pub fn bytes_to_c_char_array(bytes: &[u8]) -> [c_char; N] { let mut array = [0 as c_char; N]; - let bytes = s.as_bytes(); let len = bytes.len().min(N - 1); for (i, &byte) in bytes.iter().take(len).enumerate() { array[i] = byte as c_char; @@ -374,15 +373,16 @@ mod tests { array } - /// Helper function to convert raw bytes to a c_char array for testing invalid UTF-8 - fn bytes_to_c_char_array(bytes: &[u8]) -> [c_char; N] { - let mut array = [0 as c_char; N]; - let len = bytes.len().min(N - 1); - for (i, &byte) in bytes.iter().take(len).enumerate() { - array[i] = byte as c_char; - } - array + /// Helper function to convert a Rust string to a c_char array for testing + pub fn string_to_c_char_array(s: &str) -> [c_char; N] { + bytes_to_c_char_array(s.as_bytes()) } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::test_utils::*; #[test] fn slice_to_string_valid_utf8() { diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 080239ab..58dcd100 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -222,29 +222,9 @@ impl From for fact_api::ProcessSignal { #[cfg(test)] mod tests { use super::*; + use crate::event::test_utils::*; use std::os::raw::c_char; - /// Helper function to convert a Rust string to a c_char array for testing - fn string_to_c_char_array(s: &str) -> [c_char; N] { - let mut array = [0 as c_char; N]; - let bytes = s.as_bytes(); - let len = bytes.len().min(N - 1); - for (i, &byte) in bytes.iter().take(len).enumerate() { - array[i] = byte as c_char; - } - array - } - - /// Helper function to convert raw bytes to a c_char array for testing invalid UTF-8 - fn bytes_to_c_char_array(bytes: &[u8]) -> [c_char; N] { - let mut array = [0 as c_char; N]; - let len = bytes.len().min(N - 1); - for (i, &byte) in bytes.iter().take(len).enumerate() { - array[i] = byte as c_char; - } - array - } - /// Helper to create a default process_t for testing fn default_process_t() -> process_t { process_t { From dce40f1cbdcff440d980746b4770f227a7b41d15 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 5 Feb 2026 22:13:51 -0800 Subject: [PATCH 04/24] cargo fmt --all --- fact/src/event/mod.rs | 117 +++++++++++++++++++++++++++++++------- fact/src/event/process.rs | 60 +++++++++++++++---- 2 files changed, 144 insertions(+), 33 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index d34b395b..082f7e0f 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -381,8 +381,8 @@ mod test_utils { #[cfg(test)] mod tests { - use super::*; use super::test_utils::*; + use super::*; #[test] fn slice_to_string_valid_utf8() { @@ -400,7 +400,12 @@ mod tests { for (input, description) in tests { let arr = string_to_c_char_array::<256>(input); - assert_eq!(slice_to_string(&arr).unwrap(), input, "Failed for {}", description); + assert_eq!( + slice_to_string(&arr).unwrap(), + input, + "Failed for {}", + description + ); } } @@ -408,16 +413,31 @@ mod tests { fn slice_to_string_invalid_utf8() { let tests: &[(&[u8], &str)] = &[ (&[0xFF, 0xFE, 0xFD], "invalid continuation bytes"), - (&[b't', b'e', b's', b't', 0xE2], "truncated multi-byte sequence"), + ( + &[b't', b'e', b's', b't', 0xE2], + "truncated multi-byte sequence", + ), (&[0xC0, 0x80], "overlong encoding"), - (&[b'h', b'e', b'l', b'l', b'o', 0x80, b'w', b'o', b'r', b'l', b'd'], "invalid start byte"), + ( + &[ + b'h', b'e', b'l', b'l', b'o', 0x80, b'w', b'o', b'r', b'l', b'd', + ], + "invalid start byte", + ), (&[0x80], "lone continuation byte"), - (&[b't', b'e', b's', b't', 0xFF, 0xFE], "mixed valid and invalid bytes"), + ( + &[b't', b'e', b's', b't', 0xFF, 0xFE], + "mixed valid and invalid bytes", + ), ]; for (bytes, description) in tests { let arr = bytes_to_c_char_array::<256>(bytes); - assert!(slice_to_string(&arr).is_err(), "Should fail for {}", description); + assert!( + slice_to_string(&arr).is_err(), + "Should fail for {}", + description + ); } } @@ -426,31 +446,61 @@ mod tests { let tests = [ ("/etc/test", "/etc/test", "ASCII"), ("/tmp/файл.txt", "/tmp/файл.txt", "Cyrillic"), - ("/home/user/测试文件.log", "/home/user/测试文件.log", "Chinese"), + ( + "/home/user/测试文件.log", + "/home/user/测试文件.log", + "Chinese", + ), ("/data/🚀rocket.dat", "/data/🚀rocket.dat", "emoji"), - ("/var/log/app-данные-数据-🐛.log", "/var/log/app-данные-数据-🐛.log", "mixed Unicode"), + ( + "/var/log/app-данные-数据-🐛.log", + "/var/log/app-данные-数据-🐛.log", + "mixed Unicode", + ), ("/home/ملف.txt", "/home/ملف.txt", "Arabic"), ("/opt/ファイル.conf", "/opt/ファイル.conf", "Japanese"), ]; for (input, expected, description) in tests { let arr = string_to_c_char_array::<4096>(input); - assert_eq!(sanitize_d_path(&arr), PathBuf::from(expected), "Failed for {}", description); + assert_eq!( + sanitize_d_path(&arr), + PathBuf::from(expected), + "Failed for {}", + description + ); } } #[test] fn sanitize_d_path_deleted_suffix() { let tests = [ - ("/tmp/test.txt (deleted)", "/tmp/test.txt", "ASCII with deleted suffix"), - ("/tmp/файл.txt (deleted)", "/tmp/файл.txt", "Unicode with deleted suffix"), + ( + "/tmp/test.txt (deleted)", + "/tmp/test.txt", + "ASCII with deleted suffix", + ), + ( + "/tmp/файл.txt (deleted)", + "/tmp/файл.txt", + "Unicode with deleted suffix", + ), ("/etc/config.yaml", "/etc/config.yaml", "no deleted suffix"), - ("/var/log/app/debug.log (deleted)", "/var/log/app/debug.log", "nested path with deleted suffix"), + ( + "/var/log/app/debug.log (deleted)", + "/var/log/app/debug.log", + "nested path with deleted suffix", + ), ]; for (input, expected, description) in tests { let arr = string_to_c_char_array::<4096>(input); - assert_eq!(sanitize_d_path(&arr), PathBuf::from(expected), "Failed for {}", description); + assert_eq!( + sanitize_d_path(&arr), + PathBuf::from(expected), + "Failed for {}", + description + ); } } @@ -458,25 +508,35 @@ mod tests { fn sanitize_d_path_invalid_utf8() { let tests: &[(&[u8], &str, &str, &str)] = &[ ( - &[b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b'.', b't', b'x', b't'], + &[ + b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b'.', b't', b'x', b't', + ], "/tmp/", ".txt", "invalid continuation bytes", ), ( - &[b'/', b'v', b'a', b'r', b'/', b't', b'e', b's', b't', 0xE2, 0x80], + &[ + b'/', b'v', b'a', b'r', b'/', b't', b'e', b's', b't', 0xE2, 0x80, + ], "/var/", "", "truncated multi-byte sequence", ), ( - &[b'/', b'h', b'o', b'm', b'e', b'/', b'f', b'i', b'l', b'e', 0x80, b'.', b'l', b'o', b'g'], + &[ + b'/', b'h', b'o', b'm', b'e', b'/', b'f', b'i', b'l', b'e', 0x80, b'.', b'l', + b'o', b'g', + ], "/home/", ".log", "invalid start byte", ), ( - &[b'/', b't', b'm', b'p', b'/', 0xD1, 0x84, 0xFF, 0xD0, 0xBB, b'.', b't', b'x', b't'], + &[ + b'/', b't', b'm', b'p', b'/', 0xD1, 0x84, 0xFF, 0xD0, 0xBB, b'.', b't', b'x', + b't', + ], "/tmp/", "", "mixed valid and invalid UTF-8", @@ -488,18 +548,33 @@ mod tests { let result = sanitize_d_path(&arr); let result_str = result.to_string_lossy(); - assert!(result_str.contains(must_contain1), "Failed for {} - should contain '{}'", description, must_contain1); + assert!( + result_str.contains(must_contain1), + "Failed for {} - should contain '{}'", + description, + must_contain1 + ); if !must_contain2.is_empty() { - assert!(result_str.contains(must_contain2), "Failed for {} - should contain '{}'", description, must_contain2); + assert!( + result_str.contains(must_contain2), + "Failed for {} - should contain '{}'", + description, + must_contain2 + ); } - assert!(result_str.contains('\u{FFFD}'), "Failed for {} - should contain replacement character", description); + assert!( + result_str.contains('\u{FFFD}'), + "Failed for {} - should contain replacement character", + description + ); } } #[test] fn sanitize_d_path_invalid_utf8_with_deleted_suffix() { let invalid_with_deleted = bytes_to_c_char_array::<4096>(&[ - b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b' ', b'(', b'd', b'e', b'l', b'e', b't', b'e', b'd', b')', + b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b' ', b'(', b'd', b'e', b'l', b'e', b't', + b'e', b'd', b')', ]); let result = sanitize_d_path(&invalid_with_deleted); let result_str = result.to_string_lossy(); diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 58dcd100..89dbe4a6 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -305,7 +305,10 @@ mod tests { fn process_conversion_invalid_utf8_comm() { let tests: &[(&[u8], &str)] = &[ (&[b't', b'e', b's', b't', 0xFF, 0xFE], "invalid bytes"), - (&[b'a', b'p', b'p', 0xE2, 0x80], "truncated multi-byte sequence"), + ( + &[b'a', b'p', b'p', 0xE2, 0x80], + "truncated multi-byte sequence", + ), ]; for (bytes, description) in tests { @@ -331,7 +334,12 @@ mod tests { proc.exe_path = string_to_c_char_array::<4096>(path); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); - assert_eq!(result.unwrap().exe_path, PathBuf::from(path), "Failed for {}", description); + assert_eq!( + result.unwrap().exe_path, + PathBuf::from(path), + "Failed for {}", + description + ); } } @@ -353,9 +361,21 @@ mod tests { let tests: &[(&str, Vec<&str>, &str)] = &[ ("arg1\0arg2\0arg3\0", vec!["arg1", "arg2", "arg3"], "ASCII"), ("файл\0данные\0", vec!["файл", "данные"], "Cyrillic"), - ("测试\0文件\0数据\0", vec!["测试", "文件", "数据"], "Chinese"), - ("app\0🚀file\0📁data\0", vec!["app", "🚀file", "📁data"], "emoji"), - ("test\0файл\0测试\0🚀\0", vec!["test", "файл", "测试", "🚀"], "mixed UTF-8"), + ( + "测试\0文件\0数据\0", + vec!["测试", "文件", "数据"], + "Chinese", + ), + ( + "app\0🚀file\0📁data\0", + vec!["app", "🚀file", "📁data"], + "emoji", + ), + ( + "test\0файл\0测试\0🚀\0", + vec!["test", "файл", "测试", "🚀"], + "mixed UTF-8", + ), ]; for (args_str, expected, description) in tests { @@ -364,15 +384,28 @@ mod tests { proc.args_len = args_str.len() as u32; let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); - assert_eq!(result.unwrap().args, *expected, "Failed for {}", description); + assert_eq!( + result.unwrap().args, + *expected, + "Failed for {}", + description + ); } } #[test] fn process_conversion_invalid_utf8_args() { let tests: &[(&[u8], u32, &str)] = &[ - (&[b'a', b'r', b'g', b'1', 0, 0xFF, 0xFE, b'a', b'r', b'g', 0], 11, "invalid bytes"), - (&[b't', b'e', b's', b't', 0, 0xE2, 0x80, 0], 8, "truncated multi-byte sequence"), + ( + &[b'a', b'r', b'g', b'1', 0, 0xFF, 0xFE, b'a', b'r', b'g', 0], + 11, + "invalid bytes", + ), + ( + &[b't', b'e', b's', b't', 0, 0xE2, 0x80, 0], + 8, + "truncated multi-byte sequence", + ), ]; for (bytes, args_len, description) in tests { @@ -438,7 +471,12 @@ mod tests { assert!(result.is_ok(), "Failed for {}", description); let lineage = result.unwrap().lineage; assert_eq!(lineage.len(), 1); - assert_eq!(lineage[0].exe_path, PathBuf::from(path), "Failed for {}", description); + assert_eq!( + lineage[0].exe_path, + PathBuf::from(path), + "Failed for {}", + description + ); } } @@ -447,9 +485,7 @@ mod tests { let mut proc = default_process_t(); proc.lineage[0] = lineage_t { uid: 1000, - exe_path: bytes_to_c_char_array::<4096>(&[ - b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE, - ]), + exe_path: bytes_to_c_char_array::<4096>(&[b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE]), }; proc.lineage_len = 1; let result = Process::try_from(proc); From 928f41d9b562b0e1569eb96c56f34be0ec3b67fc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 6 Feb 2026 11:11:18 -0800 Subject: [PATCH 05/24] Reverted changes to test_multiple --- tests/test_file_open.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index 20ff4f4b..a10276fb 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -37,11 +37,7 @@ def test_open(fact, monitored_dir, server, filename): server.wait_events([e]) -@pytest.mark.parametrize("filenames", [ - ['0.txt', '1.txt', '2.txt'], - ['café.txt', 'файл.txt', '测试.txt'], -]) -def test_multiple(fact, monitored_dir, server, filenames): +def test_multiple(fact, monitored_dir, server): """ Tests the opening of multiple files and verifies that the corresponding events are captured by the server. @@ -55,8 +51,8 @@ def test_multiple(fact, monitored_dir, server, filenames): events = [] process = Process.from_proc() # File Under Test - for filename in filenames: - fut = os.path.join(monitored_dir, filename) + for i in range(3): + fut = os.path.join(monitored_dir, f'{i}.txt') with open(fut, 'w') as f: f.write('This is a test') From 92bc1e3ca0068388ab847d8388184819a3fcf123 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 6 Feb 2026 11:30:57 -0800 Subject: [PATCH 06/24] Added integration test case with invalid utf-8 --- tests/test_file_open.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index a10276fb..92af3195 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -13,6 +13,7 @@ 'файл.txt', '测试.txt', '🚀rocket.txt', + b'test\xff\xfe.txt', ]) def test_open(fact, monitored_dir, server, filename): """ From bf7cb8352bc6f810caf1eaabe5bbbabe7da2e32d Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 7 Feb 2026 19:01:06 -0800 Subject: [PATCH 07/24] Fixed how file paths are joined when there are bytes. Invalid utf-8 is replaced when comparing to the result --- tests/test_file_open.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index 92af3195..f272b8eb 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -27,12 +27,25 @@ def test_open(fact, monitored_dir, server, filename): filename: Name of the file to create (includes UTF-8 test cases). """ # File Under Test - fut = os.path.join(monitored_dir, filename) + # Handle bytes filenames by converting monitored_dir to bytes + if isinstance(filename, bytes): + fut = os.path.join(os.fsencode(monitored_dir), filename) + else: + fut = os.path.join(monitored_dir, filename) + with open(fut, 'w') as f: f.write('This is a test') + # Convert fut back to string for the Event + # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD + if isinstance(fut, bytes): + # Manually convert to match Rust's behavior: replace invalid UTF-8 with U+FFFD + fut_str = fut.decode('utf-8', errors='replace') + else: + fut_str = fut + e = Event(process=Process.from_proc(), event_type=EventType.CREATION, - file=fut, host_path='') + file=fut_str, host_path='') print(f'Waiting for event: {e}') server.wait_events([e]) From 4b4bd97fd1e1d230a0e12dfcf23b2f208119d237 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 8 Feb 2026 14:39:23 -0800 Subject: [PATCH 08/24] Added tests for test_path_chown and test_path_unlink --- tests/test_path_chmod.py | 47 +++++++++++++++++++++++++++++++++------ tests/test_path_chown.py | 25 +++++++++++++++++---- tests/test_path_unlink.py | 37 +++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index 4b62e2c2..dd5e1f36 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -1,10 +1,20 @@ import multiprocessing as mp import os +import pytest + from event import Event, EventType, Process -def test_chmod(fact, monitored_dir, server): +@pytest.mark.parametrize("filename", [ + 'chmod.txt', + 'café.txt', + 'файл.txt', + '测试.txt', + '🔒secure.txt', + b'perm\xff\xfe.txt', +]) +def test_chmod(fact, monitored_dir, server, filename): """ Tests changing permissions on a file and verifies the corresponding event is captured by the server @@ -13,18 +23,41 @@ def test_chmod(fact, monitored_dir, server): fact: Fixture for file activity (only required to be runing). monitored_dir: Temporary directory path for creating the test file. server: The server instance to communicate with. + filename: Name of the file to create (includes UTF-8 test cases). """ - # File Under Test - fut = os.path.join(monitored_dir, 'test.txt') + # Handle bytes filenames by converting monitored_dir to bytes + if isinstance(filename, bytes): + fut = os.path.join(os.fsencode(monitored_dir), filename) + else: + fut = os.path.join(monitored_dir, filename) + + # Create the file first + with open(fut, 'w') as f: + f.write('This is a test') + mode = 0o666 os.chmod(fut, mode) - e = Event(process=Process.from_proc(), event_type=EventType.PERMISSION, - file=fut, host_path=fut, mode=mode) + # Convert fut back to string for the Event + # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD + if isinstance(fut, bytes): + fut_str = fut.decode('utf-8', errors='replace') + else: + fut_str = fut - print(f'Waiting for event: {e}') + process = Process.from_proc() + # We expect both CREATION (from file creation) and PERMISSION (from chmod) + events = [ + Event(process=process, event_type=EventType.CREATION, + file=fut_str, host_path=''), + Event(process=process, event_type=EventType.PERMISSION, + file=fut_str, host_path='', mode=mode), + ] - server.wait_events([e]) + for e in events: + print(f'Waiting for event: {e}') + + server.wait_events(events) def test_multiple(fact, monitored_dir, server): diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index d318f4eb..fc9a28ca 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -1,4 +1,7 @@ import os +import shlex + +import pytest from event import Event, EventType, Process @@ -10,7 +13,14 @@ TEST_GID = 2345 -def test_chown(fact, test_container, server): +@pytest.mark.parametrize("filename", [ + 'chown.txt', + 'café.txt', + 'файл.txt', + '测试.txt', + '👤owner.txt', +]) +def test_chown(fact, test_container, server, filename): """ Execute a chown operation on a file and verifies the corresponding event is captured by the server. @@ -19,15 +29,22 @@ def test_chown(fact, test_container, server): fact: Fixture for file activity (only required to be running). test_container: A container for running commands in. server: The server instance to communicate with. + filename: Name of the file to create (includes UTF-8 test cases). """ # File Under Test - fut = '/container-dir/test.txt' + fut = f'/container-dir/{filename}' # Create the file and chown it + # Use shlex.quote to properly escape special characters for shell + fut_quoted = shlex.quote(fut) + touch_cmd_shell = f'touch {fut_quoted}' + chown_cmd_shell = f'chown {TEST_UID}:{TEST_GID} {fut_quoted}' + test_container.exec_run(touch_cmd_shell) + test_container.exec_run(chown_cmd_shell) + + # The args in the event won't have quotes (shell removes them) touch_cmd = f'touch {fut}' chown_cmd = f'chown {TEST_UID}:{TEST_GID} {fut}' - test_container.exec_run(touch_cmd) - test_container.exec_run(chown_cmd) loginuid = pow(2, 32) - 1 touch = Process(pid=None, diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 3a7cde5b..283897ea 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -2,26 +2,57 @@ import os import docker +import pytest from event import Event, EventType, Process -def test_remove(fact, test_file, server): +@pytest.mark.parametrize("filename", [ + 'remove.txt', + 'café.txt', + 'файл.txt', + '测试.txt', + '🗑️delete.txt', + b'rm\xff\xfe.txt', +]) +def test_remove(fact, monitored_dir, server, filename): """ Tests the removal of a file and verifies the corresponding event is captured by the server. Args: fact: Fixture for file activity (only required to be running). - test_file: Temporary file for testing. + monitored_dir: Temporary directory path for creating the test file. server: The server instance to communicate with. + filename: Name of the file to create and remove (includes UTF-8 test cases). """ + # Handle bytes filenames by converting monitored_dir to bytes + if isinstance(filename, bytes): + test_file = os.path.join(os.fsencode(monitored_dir), filename) + else: + test_file = os.path.join(monitored_dir, filename) + + # Create the file first + with open(test_file, 'w') as f: + f.write('This is a test') + + # Remove the file os.remove(test_file) + # Convert test_file back to string for the Event + # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD + if isinstance(test_file, bytes): + test_file_str = test_file.decode('utf-8', errors='replace') + else: + test_file_str = test_file + process = Process.from_proc() + # We expect both CREATION (from file creation) and UNLINK (from removal) events = [ + Event(process=process, event_type=EventType.CREATION, + file=test_file_str, host_path=''), Event(process=process, event_type=EventType.UNLINK, - file=test_file, host_path=test_file), + file=test_file_str, host_path=''), ] server.wait_events(events) From bf790c0566beef1daa08c469d2ac2e379221e3ca Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 8 Feb 2026 15:26:02 -0800 Subject: [PATCH 09/24] Added invalid utf-8 test case to test_path_chown --- tests/test_path_chown.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index fc9a28ca..95cb9ed0 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -19,6 +19,7 @@ 'файл.txt', '测试.txt', '👤owner.txt', + b'own\xff\xfe.txt', ]) def test_chown(fact, test_container, server, filename): """ @@ -31,8 +32,15 @@ def test_chown(fact, test_container, server, filename): server: The server instance to communicate with. filename: Name of the file to create (includes UTF-8 test cases). """ + # Handle bytes filenames - convert to string with replacement characters + # Rust will use the same replacement, so the strings will match + if isinstance(filename, bytes): + filename_str = filename.decode('utf-8', errors='replace') + else: + filename_str = filename + # File Under Test - fut = f'/container-dir/{filename}' + fut = f'/container-dir/{filename_str}' # Create the file and chown it # Use shlex.quote to properly escape special characters for shell From 6a96ed6b7577f1be7410918445732aa3863d5761 Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Mon, 9 Feb 2026 10:31:37 -0800 Subject: [PATCH 10/24] Apply suggestions from code review Co-authored-by: Mauro Ezequiel Moltrasio --- fact/src/event/mod.rs | 40 ++++++++------------------------------- fact/src/event/process.rs | 29 +++++++--------------------- tests/test_file_open.py | 16 +++++++--------- 3 files changed, 22 insertions(+), 63 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 082f7e0f..413dfc03 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -413,22 +413,11 @@ mod tests { fn slice_to_string_invalid_utf8() { let tests: &[(&[u8], &str)] = &[ (&[0xFF, 0xFE, 0xFD], "invalid continuation bytes"), - ( - &[b't', b'e', b's', b't', 0xE2], - "truncated multi-byte sequence", - ), + (b"test\xE2", "truncated multi-byte sequence"), (&[0xC0, 0x80], "overlong encoding"), - ( - &[ - b'h', b'e', b'l', b'l', b'o', 0x80, b'w', b'o', b'r', b'l', b'd', - ], - "invalid start byte", - ), + (b"hello\x80world", "invalid start byte"), (&[0x80], "lone continuation byte"), - ( - &[b't', b'e', b's', b't', 0xFF, 0xFE], - "mixed valid and invalid bytes", - ), + (b"test\xFF\xFE", "mixed valid and invalid bytes"), ]; for (bytes, description) in tests { @@ -508,35 +497,25 @@ mod tests { fn sanitize_d_path_invalid_utf8() { let tests: &[(&[u8], &str, &str, &str)] = &[ ( - &[ - b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b'.', b't', b'x', b't', - ], + b"/tmp/\xFF\xFE.txt", "/tmp/", ".txt", "invalid continuation bytes", ), ( - &[ - b'/', b'v', b'a', b'r', b'/', b't', b'e', b's', b't', 0xE2, 0x80, - ], + b"/var/test\xE2\x80", "/var/", "", "truncated multi-byte sequence", ), ( - &[ - b'/', b'h', b'o', b'm', b'e', b'/', b'f', b'i', b'l', b'e', 0x80, b'.', b'l', - b'o', b'g', - ], + b"/home/file\x80.log", "/home/", ".log", "invalid start byte", ), ( - &[ - b'/', b't', b'm', b'p', b'/', 0xD1, 0x84, 0xFF, 0xD0, 0xBB, b'.', b't', b'x', - b't', - ], + b"/tmp/\xD1\x84\xFF\xD0\xBB.txt", "/tmp/", "", "mixed valid and invalid UTF-8", @@ -572,10 +551,7 @@ mod tests { #[test] fn sanitize_d_path_invalid_utf8_with_deleted_suffix() { - let invalid_with_deleted = bytes_to_c_char_array::<4096>(&[ - b'/', b't', b'm', b'p', b'/', 0xFF, 0xFE, b' ', b'(', b'd', b'e', b'l', b'e', b't', - b'e', b'd', b')', - ]); + let invalid_with_deleted = bytes_to_c_char_array::<4096>(b"/tmp/\xFF\xFE (deleted)"); let result = sanitize_d_path(&invalid_with_deleted); let result_str = result.to_string_lossy(); diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 89dbe4a6..b4dd4f71 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -304,11 +304,8 @@ mod tests { #[test] fn process_conversion_invalid_utf8_comm() { let tests: &[(&[u8], &str)] = &[ - (&[b't', b'e', b's', b't', 0xFF, 0xFE], "invalid bytes"), - ( - &[b'a', b'p', b'p', 0xE2, 0x80], - "truncated multi-byte sequence", - ), + (b"test\xFF\xFE", "invalid bytes"), + (b"app\xE2\x80", "truncated multi-byte sequence"), ]; for (bytes, description) in tests { @@ -346,9 +343,7 @@ mod tests { #[test] fn process_conversion_invalid_utf8_exe_path() { let mut proc = default_process_t(); - proc.exe_path = bytes_to_c_char_array::<4096>(&[ - b'/', b'u', b's', b'r', b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE, - ]); + proc.exe_path = bytes_to_c_char_array::<4096>(b"/usr/bin/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_ok()); let exe_path = result.unwrap().exe_path; @@ -396,16 +391,8 @@ mod tests { #[test] fn process_conversion_invalid_utf8_args() { let tests: &[(&[u8], u32, &str)] = &[ - ( - &[b'a', b'r', b'g', b'1', 0, 0xFF, 0xFE, b'a', b'r', b'g', 0], - 11, - "invalid bytes", - ), - ( - &[b't', b'e', b's', b't', 0, 0xE2, 0x80, 0], - 8, - "truncated multi-byte sequence", - ), + (b"arg1\0\xFF\xFEarg\0", 11, "invalid bytes"), + (b"test\0\xE2\x80\0", 8, "truncated multi-byte sequence"), ]; for (bytes, args_len, description) in tests { @@ -445,9 +432,7 @@ mod tests { #[test] fn process_conversion_invalid_utf8_memory_cgroup() { let mut proc = default_process_t(); - proc.memory_cgroup = bytes_to_c_char_array::<4096>(&[ - b'/', b'd', b'o', b'c', b'k', b'e', b'r', b'/', 0xFF, 0xFE, - ]); + proc.memory_cgroup = bytes_to_c_char_array::<4096>(b"/docker/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_err()); } @@ -485,7 +470,7 @@ mod tests { let mut proc = default_process_t(); proc.lineage[0] = lineage_t { uid: 1000, - exe_path: bytes_to_c_char_array::<4096>(&[b'/', b'b', b'i', b'n', b'/', 0xFF, 0xFE]), + exe_path: bytes_to_c_char_array::<4096>(b"/bin/\xFF\xFE"), }; proc.lineage_len = 1; let result = Process::try_from(proc); diff --git a/tests/test_file_open.py b/tests/test_file_open.py index f272b8eb..07ff1dc5 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -8,12 +8,12 @@ @pytest.mark.parametrize("filename", [ - 'create.txt', - 'café.txt', - 'файл.txt', - '测试.txt', - '🚀rocket.txt', - b'test\xff\xfe.txt', + pytest.param('create.txt', id='ascii'), + pytest.param('café.txt', id='spanish'), + pytest.param('файл.txt', id='cyrilic'), + pytest.param('测试.txt', id='chinese'), + pytest.param('🚀rocket.txt', id='emoji'), + pytest.param(b'test\xff\xfe.txt', id='invalid'), ]) def test_open(fact, monitored_dir, server, filename): """ @@ -40,9 +40,7 @@ def test_open(fact, monitored_dir, server, filename): # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD if isinstance(fut, bytes): # Manually convert to match Rust's behavior: replace invalid UTF-8 with U+FFFD - fut_str = fut.decode('utf-8', errors='replace') - else: - fut_str = fut + fut = fut.decode('utf-8', errors='replace') e = Event(process=Process.from_proc(), event_type=EventType.CREATION, file=fut_str, host_path='') From d1a1cf82f7f1f9d3d9584249aee9ac5e447aa033 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 9 Feb 2026 14:30:38 -0800 Subject: [PATCH 11/24] Created helper functions join_path_with_filename and path_to_string --- tests/conftest.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_file_open.py | 14 ++++---------- tests/test_path_chmod.py | 15 ++++----------- tests/test_path_chown.py | 9 +++------ tests/test_path_unlink.py | 15 ++++----------- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f23d4dbe..aee04534 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,43 @@ 'test_editors.commons' ] +def join_path_with_filename(directory, filename): + """ + Join a directory path with a filename, handling bytes filenames properly. + + When filename is bytes (e.g., containing invalid UTF-8), converts the + directory to bytes before joining to avoid mixing str and bytes. + + Args: + directory: Directory path (str) + filename: Filename (str or bytes) + + Returns: + Joined path (str or bytes, matching the filename type) + """ + if isinstance(filename, bytes): + return os.path.join(os.fsencode(directory), filename) + else: + return os.path.join(directory, filename) + + +def path_to_string(path): + """ + Convert a filesystem path to string, replacing invalid UTF-8 with U+FFFD. + + This matches the behavior of Rust's String::from_utf8_lossy() used in + the fact codebase. + + Args: + path: Filesystem path (str or bytes) + + Returns: + String representation with invalid UTF-8 replaced by replacement character + """ + if isinstance(path, bytes): + return path.decode('utf-8', errors='replace') + else: + return path @pytest.fixture def monitored_dir(): diff --git a/tests/test_file_open.py b/tests/test_file_open.py index 07ff1dc5..c3fd252e 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -4,6 +4,7 @@ import docker import pytest +from conftest import join_path_with_filename, path_to_string from event import Event, EventType, Process @@ -27,20 +28,13 @@ def test_open(fact, monitored_dir, server, filename): filename: Name of the file to create (includes UTF-8 test cases). """ # File Under Test - # Handle bytes filenames by converting monitored_dir to bytes - if isinstance(filename, bytes): - fut = os.path.join(os.fsencode(monitored_dir), filename) - else: - fut = os.path.join(monitored_dir, filename) + fut = join_path_with_filename(monitored_dir, filename) with open(fut, 'w') as f: f.write('This is a test') - # Convert fut back to string for the Event - # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD - if isinstance(fut, bytes): - # Manually convert to match Rust's behavior: replace invalid UTF-8 with U+FFFD - fut = fut.decode('utf-8', errors='replace') + # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD + fut_str = path_to_string(fut) e = Event(process=Process.from_proc(), event_type=EventType.CREATION, file=fut_str, host_path='') diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index dd5e1f36..152d5bd8 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -3,6 +3,7 @@ import pytest +from conftest import join_path_with_filename, path_to_string from event import Event, EventType, Process @@ -25,11 +26,7 @@ def test_chmod(fact, monitored_dir, server, filename): server: The server instance to communicate with. filename: Name of the file to create (includes UTF-8 test cases). """ - # Handle bytes filenames by converting monitored_dir to bytes - if isinstance(filename, bytes): - fut = os.path.join(os.fsencode(monitored_dir), filename) - else: - fut = os.path.join(monitored_dir, filename) + fut = join_path_with_filename(monitored_dir, filename) # Create the file first with open(fut, 'w') as f: @@ -38,12 +35,8 @@ def test_chmod(fact, monitored_dir, server, filename): mode = 0o666 os.chmod(fut, mode) - # Convert fut back to string for the Event - # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD - if isinstance(fut, bytes): - fut_str = fut.decode('utf-8', errors='replace') - else: - fut_str = fut + # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD + fut_str = path_to_string(fut) process = Process.from_proc() # We expect both CREATION (from file creation) and PERMISSION (from chmod) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 95cb9ed0..374af17c 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -3,6 +3,7 @@ import pytest +from conftest import path_to_string from event import Event, EventType, Process # Tests here have to use a container to do 'chown', @@ -32,12 +33,8 @@ def test_chown(fact, test_container, server, filename): server: The server instance to communicate with. filename: Name of the file to create (includes UTF-8 test cases). """ - # Handle bytes filenames - convert to string with replacement characters - # Rust will use the same replacement, so the strings will match - if isinstance(filename, bytes): - filename_str = filename.decode('utf-8', errors='replace') - else: - filename_str = filename + # Convert filename to string, replacing invalid UTF-8 with U+FFFD + filename_str = path_to_string(filename) # File Under Test fut = f'/container-dir/{filename_str}' diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 283897ea..3e6395f5 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -4,6 +4,7 @@ import docker import pytest +from conftest import join_path_with_filename, path_to_string from event import Event, EventType, Process @@ -26,11 +27,7 @@ def test_remove(fact, monitored_dir, server, filename): server: The server instance to communicate with. filename: Name of the file to create and remove (includes UTF-8 test cases). """ - # Handle bytes filenames by converting monitored_dir to bytes - if isinstance(filename, bytes): - test_file = os.path.join(os.fsencode(monitored_dir), filename) - else: - test_file = os.path.join(monitored_dir, filename) + test_file = join_path_with_filename(monitored_dir, filename) # Create the file first with open(test_file, 'w') as f: @@ -39,12 +36,8 @@ def test_remove(fact, monitored_dir, server, filename): # Remove the file os.remove(test_file) - # Convert test_file back to string for the Event - # For bytes paths with invalid UTF-8, Rust will use the replacement character U+FFFD - if isinstance(test_file, bytes): - test_file_str = test_file.decode('utf-8', errors='replace') - else: - test_file_str = test_file + # Convert test_file to string for the Event, replacing invalid UTF-8 with U+FFFD + test_file_str = path_to_string(test_file) process = Process.from_proc() # We expect both CREATION (from file creation) and UNLINK (from removal) From f613f1c4a5279f591c67faa7240df2f670a597fc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 9 Feb 2026 15:35:10 -0800 Subject: [PATCH 12/24] Using { PATH_MAX as usize } instead of other values for the template --- fact/src/event/mod.rs | 13 +++++++------ fact/src/event/process.rs | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 413dfc03..8264029b 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -399,7 +399,7 @@ mod tests { ]; for (input, description) in tests { - let arr = string_to_c_char_array::<256>(input); + let arr = string_to_c_char_array::<{ PATH_MAX as usize }>(input); assert_eq!( slice_to_string(&arr).unwrap(), input, @@ -421,7 +421,7 @@ mod tests { ]; for (bytes, description) in tests { - let arr = bytes_to_c_char_array::<256>(bytes); + let arr = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); assert!( slice_to_string(&arr).is_err(), "Should fail for {}", @@ -451,7 +451,7 @@ mod tests { ]; for (input, expected, description) in tests { - let arr = string_to_c_char_array::<4096>(input); + let arr = string_to_c_char_array::<{ PATH_MAX as usize }>(input); assert_eq!( sanitize_d_path(&arr), PathBuf::from(expected), @@ -483,7 +483,7 @@ mod tests { ]; for (input, expected, description) in tests { - let arr = string_to_c_char_array::<4096>(input); + let arr = string_to_c_char_array::<{ PATH_MAX as usize }>(input); assert_eq!( sanitize_d_path(&arr), PathBuf::from(expected), @@ -523,7 +523,7 @@ mod tests { ]; for (bytes, must_contain1, must_contain2, description) in tests { - let arr = bytes_to_c_char_array::<4096>(bytes); + let arr = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); let result = sanitize_d_path(&arr); let result_str = result.to_string_lossy(); @@ -551,7 +551,8 @@ mod tests { #[test] fn sanitize_d_path_invalid_utf8_with_deleted_suffix() { - let invalid_with_deleted = bytes_to_c_char_array::<4096>(b"/tmp/\xFF\xFE (deleted)"); + let invalid_with_deleted = + bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/tmp/\xFF\xFE (deleted)"); let result = sanitize_d_path(&invalid_with_deleted); let result_str = result.to_string_lossy(); diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index b4dd4f71..ce5131df 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -1,6 +1,6 @@ use std::{ffi::CStr, path::PathBuf}; -use fact_ebpf::{lineage_t, process_t}; +use fact_ebpf::{lineage_t, process_t, PATH_MAX}; use serde::Serialize; use uuid::Uuid; @@ -229,17 +229,17 @@ mod tests { fn default_process_t() -> process_t { process_t { comm: string_to_c_char_array::<16>("test"), - args: string_to_c_char_array::<4096>("arg1\0arg2\0"), + args: string_to_c_char_array::<{ PATH_MAX as usize }>("arg1\0arg2\0"), args_len: 10, - exe_path: string_to_c_char_array::<4096>("/usr/bin/test"), - memory_cgroup: string_to_c_char_array::<4096>("init.scope"), + exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>("/usr/bin/test"), + memory_cgroup: string_to_c_char_array::<{ PATH_MAX as usize }>("init.scope"), uid: 1000, gid: 1000, login_uid: 1000, pid: 12345, lineage: [lineage_t { uid: 1000, - exe_path: string_to_c_char_array::<4096>("/bin/bash"), + exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>("/bin/bash"), }; 2], lineage_len: 0, in_root_mount_ns: 1, @@ -328,7 +328,7 @@ mod tests { for (path, description) in tests { let mut proc = default_process_t(); - proc.exe_path = string_to_c_char_array::<4096>(path); + proc.exe_path = string_to_c_char_array::<{ PATH_MAX as usize }>(path); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); assert_eq!( @@ -343,7 +343,7 @@ mod tests { #[test] fn process_conversion_invalid_utf8_exe_path() { let mut proc = default_process_t(); - proc.exe_path = bytes_to_c_char_array::<4096>(b"/usr/bin/\xFF\xFE"); + proc.exe_path = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/usr/bin/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_ok()); let exe_path = result.unwrap().exe_path; @@ -375,7 +375,7 @@ mod tests { for (args_str, expected, description) in tests { let mut proc = default_process_t(); - proc.args = string_to_c_char_array::<4096>(args_str); + proc.args = string_to_c_char_array::<{ PATH_MAX as usize }>(args_str); proc.args_len = args_str.len() as u32; let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); @@ -397,7 +397,7 @@ mod tests { for (bytes, args_len, description) in tests { let mut proc = default_process_t(); - proc.args = bytes_to_c_char_array::<4096>(bytes); + proc.args = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); proc.args_len = *args_len; let result = Process::try_from(proc); assert!(result.is_err(), "Should fail for {}", description); @@ -417,7 +417,7 @@ mod tests { for (cgroup, expected_id, description) in tests { let mut proc = default_process_t(); - proc.memory_cgroup = string_to_c_char_array::<4096>(cgroup); + proc.memory_cgroup = string_to_c_char_array::<{ PATH_MAX as usize }>(cgroup); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); assert_eq!( @@ -432,7 +432,7 @@ mod tests { #[test] fn process_conversion_invalid_utf8_memory_cgroup() { let mut proc = default_process_t(); - proc.memory_cgroup = bytes_to_c_char_array::<4096>(b"/docker/\xFF\xFE"); + proc.memory_cgroup = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/docker/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_err()); } @@ -449,7 +449,7 @@ mod tests { let mut proc = default_process_t(); proc.lineage[0] = lineage_t { uid: 1000, - exe_path: string_to_c_char_array::<4096>(path), + exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>(path), }; proc.lineage_len = 1; let result = Process::try_from(proc); @@ -470,7 +470,7 @@ mod tests { let mut proc = default_process_t(); proc.lineage[0] = lineage_t { uid: 1000, - exe_path: bytes_to_c_char_array::<4096>(b"/bin/\xFF\xFE"), + exe_path: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/bin/\xFF\xFE"), }; proc.lineage_len = 1; let result = Process::try_from(proc); From cfbcae46f9e6c51010642c5fe97450fdb73b647c Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 9 Feb 2026 20:05:05 -0800 Subject: [PATCH 13/24] Using regex instead of checking for contains --- Cargo.lock | 1 + Cargo.toml | 1 + fact/Cargo.toml | 1 + fact/src/event/mod.rs | 39 +++++++++++++-------------------------- fact/src/event/process.rs | 24 ++++++++++++++++++++---- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 729a22c1..18d77516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,6 +454,7 @@ dependencies = [ "prometheus-client", "prost", "prost-types", + "regex", "serde", "serde_json", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 143bd750..742c25f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ uuid = { version = "1.17.0", features = ["v4"] } bindgen = "0.72.0" tempfile = { version = "3.20.0", default-features = false } yaml-rust2 = "0.11.0" +regex = "1.11.1" [profile.release] debug = "line-tables-only" diff --git a/fact/Cargo.toml b/fact/Cargo.toml index 103e37a2..3b84db24 100644 --- a/fact/Cargo.toml +++ b/fact/Cargo.toml @@ -35,6 +35,7 @@ fact-ebpf = { path = "../fact-ebpf" } [dev-dependencies] tempfile = { workspace = true } +regex = { workspace = true } [build-dependencies] anyhow = { workspace = true } diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 8264029b..da5df850 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -495,56 +495,43 @@ mod tests { #[test] fn sanitize_d_path_invalid_utf8() { - let tests: &[(&[u8], &str, &str, &str)] = &[ + use regex::Regex; + + let tests: &[(&[u8], &str, &str)] = &[ ( b"/tmp/\xFF\xFE.txt", - "/tmp/", - ".txt", + r"^/tmp/\u{FFFD}+\.txt$", "invalid continuation bytes", ), ( b"/var/test\xE2\x80", - "/var/", - "", + r"^/var/test\u{FFFD}+$", "truncated multi-byte sequence", ), ( b"/home/file\x80.log", - "/home/", - ".log", + r"^/home/file\u{FFFD}\.log$", "invalid start byte", ), ( b"/tmp/\xD1\x84\xFF\xD0\xBB.txt", - "/tmp/", - "", + r"^/tmp/ф\u{FFFD}л\.txt$", "mixed valid and invalid UTF-8", ), ]; - for (bytes, must_contain1, must_contain2, description) in tests { + for (bytes, pattern, description) in tests { let arr = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); let result = sanitize_d_path(&arr); let result_str = result.to_string_lossy(); + let re = Regex::new(pattern).expect("Invalid regex pattern"); assert!( - result_str.contains(must_contain1), - "Failed for {} - should contain '{}'", + re.is_match(&result_str), + "Failed for {}: expected pattern '{}', got '{}'", description, - must_contain1 - ); - if !must_contain2.is_empty() { - assert!( - result_str.contains(must_contain2), - "Failed for {} - should contain '{}'", - description, - must_contain2 - ); - } - assert!( - result_str.contains('\u{FFFD}'), - "Failed for {} - should contain replacement character", - description + pattern, + result_str ); } } diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index ce5131df..1ede88ed 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -342,13 +342,21 @@ mod tests { #[test] fn process_conversion_invalid_utf8_exe_path() { + use regex::Regex; + let mut proc = default_process_t(); proc.exe_path = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/usr/bin/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_ok()); let exe_path = result.unwrap().exe_path; - assert!(exe_path.to_string_lossy().contains("/usr/bin/")); - assert!(exe_path.to_string_lossy().contains('\u{FFFD}')); + let exe_path_str = exe_path.to_string_lossy(); + + let re = Regex::new(r"^/usr/bin/\u{FFFD}+$").expect("Invalid regex pattern"); + assert!( + re.is_match(&exe_path_str), + "Expected pattern '^/usr/bin/\\u{{FFFD}}+$', got '{}'", + exe_path_str + ); } #[test] @@ -467,6 +475,8 @@ mod tests { #[test] fn process_conversion_invalid_utf8_lineage() { + use regex::Regex; + let mut proc = default_process_t(); proc.lineage[0] = lineage_t { uid: 1000, @@ -476,7 +486,13 @@ mod tests { let result = Process::try_from(proc); assert!(result.is_ok()); let lineage = result.unwrap().lineage; - assert!(lineage[0].exe_path.to_string_lossy().contains("/bin/")); - assert!(lineage[0].exe_path.to_string_lossy().contains('\u{FFFD}')); + let lineage_path_str = lineage[0].exe_path.to_string_lossy(); + + let re = Regex::new(r"^/bin/\u{FFFD}+$").expect("Invalid regex pattern"); + assert!( + re.is_match(&lineage_path_str), + "Expected pattern '^/bin/\\u{{FFFD}}+$', got '{}'", + lineage_path_str + ); } } From ca13e31941a1e0e1e562be85edd21ef003eed4cc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 9 Feb 2026 20:15:16 -0800 Subject: [PATCH 14/24] Removed default_process_t --- fact/src/event/process.rs | 40 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 1ede88ed..9ecb5632 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -225,26 +225,6 @@ mod tests { use crate::event::test_utils::*; use std::os::raw::c_char; - /// Helper to create a default process_t for testing - fn default_process_t() -> process_t { - process_t { - comm: string_to_c_char_array::<16>("test"), - args: string_to_c_char_array::<{ PATH_MAX as usize }>("arg1\0arg2\0"), - args_len: 10, - exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>("/usr/bin/test"), - memory_cgroup: string_to_c_char_array::<{ PATH_MAX as usize }>("init.scope"), - uid: 1000, - gid: 1000, - login_uid: 1000, - pid: 12345, - lineage: [lineage_t { - uid: 1000, - exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>("/bin/bash"), - }; 2], - lineage_len: 0, - in_root_mount_ns: 1, - } - } #[test] fn extract_container_id() { @@ -293,7 +273,7 @@ mod tests { ]; for (comm, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.comm = string_to_c_char_array::<16>(comm); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); @@ -309,7 +289,7 @@ mod tests { ]; for (bytes, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.comm = bytes_to_c_char_array::<16>(bytes); let result = Process::try_from(proc); assert!(result.is_err(), "Should fail for {}", description); @@ -327,7 +307,7 @@ mod tests { ]; for (path, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.exe_path = string_to_c_char_array::<{ PATH_MAX as usize }>(path); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); @@ -344,7 +324,7 @@ mod tests { fn process_conversion_invalid_utf8_exe_path() { use regex::Regex; - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.exe_path = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/usr/bin/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_ok()); @@ -382,7 +362,7 @@ mod tests { ]; for (args_str, expected, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.args = string_to_c_char_array::<{ PATH_MAX as usize }>(args_str); proc.args_len = args_str.len() as u32; let result = Process::try_from(proc); @@ -404,7 +384,7 @@ mod tests { ]; for (bytes, args_len, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.args = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); proc.args_len = *args_len; let result = Process::try_from(proc); @@ -424,7 +404,7 @@ mod tests { ]; for (cgroup, expected_id, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.memory_cgroup = string_to_c_char_array::<{ PATH_MAX as usize }>(cgroup); let result = Process::try_from(proc); assert!(result.is_ok(), "Failed for {}", description); @@ -439,7 +419,7 @@ mod tests { #[test] fn process_conversion_invalid_utf8_memory_cgroup() { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.memory_cgroup = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/docker/\xFF\xFE"); let result = Process::try_from(proc); assert!(result.is_err()); @@ -454,7 +434,7 @@ mod tests { ]; for (path, description) in tests { - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.lineage[0] = lineage_t { uid: 1000, exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>(path), @@ -477,7 +457,7 @@ mod tests { fn process_conversion_invalid_utf8_lineage() { use regex::Regex; - let mut proc = default_process_t(); + let mut proc = process_t::default(); proc.lineage[0] = lineage_t { uid: 1000, exe_path: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/bin/\xFF\xFE"), From 58104a3d4853a463ca2c63e57ef91616bfd98cff Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 10 Feb 2026 10:52:04 -0800 Subject: [PATCH 15/24] Refactored unit tests in fact/src/event/process.rs --- fact/src/event/process.rs | 164 +++++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 9ecb5632..10f7b8a6 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -1,6 +1,6 @@ use std::{ffi::CStr, path::PathBuf}; -use fact_ebpf::{lineage_t, process_t, PATH_MAX}; +use fact_ebpf::{lineage_t, process_t}; use serde::Serialize; use uuid::Uuid; @@ -8,7 +8,7 @@ use crate::host_info; use super::{sanitize_d_path, slice_to_string}; -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct Lineage { uid: u32, exe_path: PathBuf, @@ -223,7 +223,7 @@ impl From for fact_api::ProcessSignal { mod tests { use super::*; use crate::event::test_utils::*; - use std::os::raw::c_char; + use fact_ebpf::PATH_MAX; #[test] @@ -273,11 +273,16 @@ mod tests { ]; for (comm, description) in tests { - let mut proc = process_t::default(); - proc.comm = string_to_c_char_array::<16>(comm); - let result = Process::try_from(proc); - assert!(result.is_ok(), "Failed for {}", description); - assert_eq!(result.unwrap().comm, comm, "Failed for {}", description); + let proc = process_t { + comm: string_to_c_char_array::<16>(comm), + ..Default::default() + }; + let result = Process::try_from(proc).expect("Failed to parse process"); + let expected = Process { + comm: comm.to_string(), + ..Default::default() + }; + assert_eq!(result, expected, "Failed for {}", description); } } @@ -289,8 +294,10 @@ mod tests { ]; for (bytes, description) in tests { - let mut proc = process_t::default(); - proc.comm = bytes_to_c_char_array::<16>(bytes); + let proc = process_t { + comm: bytes_to_c_char_array::<16>(bytes), + ..Default::default() + }; let result = Process::try_from(proc); assert!(result.is_err(), "Should fail for {}", description); } @@ -307,16 +314,16 @@ mod tests { ]; for (path, description) in tests { - let mut proc = process_t::default(); - proc.exe_path = string_to_c_char_array::<{ PATH_MAX as usize }>(path); - let result = Process::try_from(proc); - assert!(result.is_ok(), "Failed for {}", description); - assert_eq!( - result.unwrap().exe_path, - PathBuf::from(path), - "Failed for {}", - description - ); + let proc = process_t { + exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>(path), + ..Default::default() + }; + let result = Process::try_from(proc).expect("Failed to parse process"); + let expected = Process { + exe_path: PathBuf::from(path), + ..Default::default() + }; + assert_eq!(result, expected, "Failed for {}", description); } } @@ -324,12 +331,12 @@ mod tests { fn process_conversion_invalid_utf8_exe_path() { use regex::Regex; - let mut proc = process_t::default(); - proc.exe_path = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/usr/bin/\xFF\xFE"); - let result = Process::try_from(proc); - assert!(result.is_ok()); - let exe_path = result.unwrap().exe_path; - let exe_path_str = exe_path.to_string_lossy(); + let proc = process_t { + exe_path: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/usr/bin/\xFF\xFE"), + ..Default::default() + }; + let result = Process::try_from(proc).expect("Failed to parse process"); + let exe_path_str = result.exe_path.to_string_lossy(); let re = Regex::new(r"^/usr/bin/\u{FFFD}+$").expect("Invalid regex pattern"); assert!( @@ -362,17 +369,17 @@ mod tests { ]; for (args_str, expected, description) in tests { - let mut proc = process_t::default(); - proc.args = string_to_c_char_array::<{ PATH_MAX as usize }>(args_str); - proc.args_len = args_str.len() as u32; - let result = Process::try_from(proc); - assert!(result.is_ok(), "Failed for {}", description); - assert_eq!( - result.unwrap().args, - *expected, - "Failed for {}", - description - ); + let proc = process_t { + args: string_to_c_char_array::<{ PATH_MAX as usize }>(args_str), + args_len: args_str.len() as u32, + ..Default::default() + }; + let result = Process::try_from(proc).expect("Failed to parse process"); + let expected_process = Process { + args: expected.iter().map(|s| s.to_string()).collect(), + ..Default::default() + }; + assert_eq!(result, expected_process, "Failed for {}", description); } } @@ -384,9 +391,11 @@ mod tests { ]; for (bytes, args_len, description) in tests { - let mut proc = process_t::default(); - proc.args = bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes); - proc.args_len = *args_len; + let proc = process_t { + args: bytes_to_c_char_array::<{ PATH_MAX as usize }>(bytes), + args_len: *args_len, + ..Default::default() + }; let result = Process::try_from(proc); assert!(result.is_err(), "Should fail for {}", description); } @@ -404,23 +413,25 @@ mod tests { ]; for (cgroup, expected_id, description) in tests { - let mut proc = process_t::default(); - proc.memory_cgroup = string_to_c_char_array::<{ PATH_MAX as usize }>(cgroup); - let result = Process::try_from(proc); - assert!(result.is_ok(), "Failed for {}", description); - assert_eq!( - result.unwrap().container_id, - expected_id.map(|s| s.to_string()), - "Failed for {}", - description - ); + let proc = process_t { + memory_cgroup: string_to_c_char_array::<{ PATH_MAX as usize }>(cgroup), + ..Default::default() + }; + let result = Process::try_from(proc).expect("Failed to parse process"); + let expected_process = Process { + container_id: expected_id.map(|s| s.to_string()), + ..Default::default() + }; + assert_eq!(result, expected_process, "Failed for {}", description); } } #[test] fn process_conversion_invalid_utf8_memory_cgroup() { - let mut proc = process_t::default(); - proc.memory_cgroup = bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/docker/\xFF\xFE"); + let proc = process_t { + memory_cgroup: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/docker/\xFF\xFE"), + ..Default::default() + }; let result = Process::try_from(proc); assert!(result.is_err()); } @@ -434,22 +445,26 @@ mod tests { ]; for (path, description) in tests { - let mut proc = process_t::default(); - proc.lineage[0] = lineage_t { - uid: 1000, - exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>(path), + let proc = process_t { + lineage: [ + lineage_t { + uid: 1000, + exe_path: string_to_c_char_array::<{ PATH_MAX as usize }>(path), + }, + Default::default(), + ], + lineage_len: 1, + ..Default::default() }; - proc.lineage_len = 1; - let result = Process::try_from(proc); - assert!(result.is_ok(), "Failed for {}", description); - let lineage = result.unwrap().lineage; - assert_eq!(lineage.len(), 1); - assert_eq!( - lineage[0].exe_path, - PathBuf::from(path), - "Failed for {}", - description - ); + let result = Process::try_from(proc).expect("Failed to parse process"); + let expected_process = Process { + lineage: vec![Lineage { + uid: 1000, + exe_path: PathBuf::from(path), + }], + ..Default::default() + }; + assert_eq!(result, expected_process, "Failed for {}", description); } } @@ -457,12 +472,17 @@ mod tests { fn process_conversion_invalid_utf8_lineage() { use regex::Regex; - let mut proc = process_t::default(); - proc.lineage[0] = lineage_t { - uid: 1000, - exe_path: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/bin/\xFF\xFE"), + let proc = process_t { + lineage: [ + lineage_t { + uid: 1000, + exe_path: bytes_to_c_char_array::<{ PATH_MAX as usize }>(b"/bin/\xFF\xFE"), + }, + Default::default(), + ], + lineage_len: 1, + ..Default::default() }; - proc.lineage_len = 1; let result = Process::try_from(proc); assert!(result.is_ok()); let lineage = result.unwrap().lineage; From b989782f5a444d5c8a4eb17f75164bd1000e03d2 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 10 Feb 2026 11:28:50 -0800 Subject: [PATCH 16/24] Test case names are consistent and uppercase --- fact/src/event/mod.rs | 34 +++++++++++++++++----------------- fact/src/event/process.rs | 18 +++++++++--------- tests/test_file_open.py | 12 ++++++------ 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index da5df850..463fb2c7 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -388,11 +388,11 @@ mod tests { fn slice_to_string_valid_utf8() { let tests = [ ("hello", "ASCII"), - ("café", "Latin-1 supplement"), + ("café", "French"), ("файл", "Cyrillic"), ("测试文件", "Chinese"), - ("test🚀file", "emoji"), - ("test-файл-测试-🐛.txt", "mixed characters"), + ("test🚀file", "Emoji"), + ("test-файл-测试-🐛.txt", "Mixed Unicode"), ("ملف", "Arabic"), ("קובץ", "Hebrew"), ("ファイル", "Japanese"), @@ -412,12 +412,12 @@ mod tests { #[test] fn slice_to_string_invalid_utf8() { let tests: &[(&[u8], &str)] = &[ - (&[0xFF, 0xFE, 0xFD], "invalid continuation bytes"), - (b"test\xE2", "truncated multi-byte sequence"), - (&[0xC0, 0x80], "overlong encoding"), - (b"hello\x80world", "invalid start byte"), - (&[0x80], "lone continuation byte"), - (b"test\xFF\xFE", "mixed valid and invalid bytes"), + (&[0xFF, 0xFE, 0xFD], "Invalid continuation bytes"), + (b"test\xE2", "Truncated multi-byte sequence"), + (&[0xC0, 0x80], "Overlong encoding"), + (b"hello\x80world", "Invalid start byte"), + (&[0x80], "Lone continuation byte"), + (b"test\xFF\xFE", "Mixed valid and invalid bytes"), ]; for (bytes, description) in tests { @@ -440,11 +440,11 @@ mod tests { "/home/user/测试文件.log", "Chinese", ), - ("/data/🚀rocket.dat", "/data/🚀rocket.dat", "emoji"), + ("/data/🚀rocket.dat", "/data/🚀rocket.dat", "Emoji"), ( "/var/log/app-данные-数据-🐛.log", "/var/log/app-данные-数据-🐛.log", - "mixed Unicode", + "Mixed Unicode", ), ("/home/ملف.txt", "/home/ملف.txt", "Arabic"), ("/opt/ファイル.conf", "/opt/ファイル.conf", "Japanese"), @@ -474,11 +474,11 @@ mod tests { "/tmp/файл.txt", "Unicode with deleted suffix", ), - ("/etc/config.yaml", "/etc/config.yaml", "no deleted suffix"), + ("/etc/config.yaml", "/etc/config.yaml", "No deleted suffix"), ( "/var/log/app/debug.log (deleted)", "/var/log/app/debug.log", - "nested path with deleted suffix", + "Nested path with deleted suffix", ), ]; @@ -501,22 +501,22 @@ mod tests { ( b"/tmp/\xFF\xFE.txt", r"^/tmp/\u{FFFD}+\.txt$", - "invalid continuation bytes", + "Invalid continuation bytes", ), ( b"/var/test\xE2\x80", r"^/var/test\u{FFFD}+$", - "truncated multi-byte sequence", + "Truncated multi-byte sequence", ), ( b"/home/file\x80.log", r"^/home/file\u{FFFD}\.log$", - "invalid start byte", + "Invalid start byte", ), ( b"/tmp/\xD1\x84\xFF\xD0\xBB.txt", r"^/tmp/ф\u{FFFD}л\.txt$", - "mixed valid and invalid UTF-8", + "Mixed valid and invalid UTF-8", ), ]; diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 10f7b8a6..dbddca1b 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -269,7 +269,7 @@ mod tests { ("test", "ASCII"), ("тест", "Cyrillic"), ("测试", "Chinese"), - ("app🚀", "emoji"), + ("app🚀", "Emoji"), ]; for (comm, description) in tests { @@ -289,8 +289,8 @@ mod tests { #[test] fn process_conversion_invalid_utf8_comm() { let tests: &[(&[u8], &str)] = &[ - (b"test\xFF\xFE", "invalid bytes"), - (b"app\xE2\x80", "truncated multi-byte sequence"), + (b"test\xFF\xFE", "Invalid bytes"), + (b"app\xE2\x80", "Truncated multi-byte sequence"), ]; for (bytes, description) in tests { @@ -309,8 +309,8 @@ mod tests { ("/usr/bin/test", "ASCII"), ("/usr/bin/тест", "Cyrillic"), ("/opt/应用/测试", "Chinese"), - ("/home/user/🚀app", "emoji"), - ("/var/app-данные-数据/bin", "mixed UTF-8"), + ("/home/user/🚀app", "Emoji"), + ("/var/app-данные-数据/bin", "Mixed UTF-8"), ]; for (path, description) in tests { @@ -359,12 +359,12 @@ mod tests { ( "app\0🚀file\0📁data\0", vec!["app", "🚀file", "📁data"], - "emoji", + "Emoji", ), ( "test\0файл\0测试\0🚀\0", vec!["test", "файл", "测试", "🚀"], - "mixed UTF-8", + "Mixed UTF-8", ), ]; @@ -386,8 +386,8 @@ mod tests { #[test] fn process_conversion_invalid_utf8_args() { let tests: &[(&[u8], u32, &str)] = &[ - (b"arg1\0\xFF\xFEarg\0", 11, "invalid bytes"), - (b"test\0\xE2\x80\0", 8, "truncated multi-byte sequence"), + (b"arg1\0\xFF\xFEarg\0", 11, "Invalid bytes"), + (b"test\0\xE2\x80\0", 8, "Truncated multi-byte sequence"), ]; for (bytes, args_len, description) in tests { diff --git a/tests/test_file_open.py b/tests/test_file_open.py index c3fd252e..b212cba2 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -9,12 +9,12 @@ @pytest.mark.parametrize("filename", [ - pytest.param('create.txt', id='ascii'), - pytest.param('café.txt', id='spanish'), - pytest.param('файл.txt', id='cyrilic'), - pytest.param('测试.txt', id='chinese'), - pytest.param('🚀rocket.txt', id='emoji'), - pytest.param(b'test\xff\xfe.txt', id='invalid'), + pytest.param('create.txt', id='ASCII'), + pytest.param('café.txt', id='French'), + pytest.param('файл.txt', id='Cyrillic'), + pytest.param('测试.txt', id='Chinese'), + pytest.param('🚀rocket.txt', id='Emoji'), + pytest.param(b'test\xff\xfe.txt', id='Invalid'), ]) def test_open(fact, monitored_dir, server, filename): """ From 17f0d46cf4edaf645bc8494459e43ac84efcba17 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 10 Feb 2026 11:30:40 -0800 Subject: [PATCH 17/24] cargo fmt --check --- fact/src/event/process.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index dbddca1b..5e42e530 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -225,7 +225,6 @@ mod tests { use crate::event::test_utils::*; use fact_ebpf::PATH_MAX; - #[test] fn extract_container_id() { let tests = [ From 52f1836719fc9bdabbba7e827b344a57c0d97f34 Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Wed, 11 Feb 2026 08:44:41 -0800 Subject: [PATCH 18/24] Apply suggestion from @Molter73 Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_chmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index 152d5bd8..f9266b5b 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -36,7 +36,7 @@ def test_chmod(fact, monitored_dir, server, filename): os.chmod(fut, mode) # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD - fut_str = path_to_string(fut) + fut = path_to_string(fut) process = Process.from_proc() # We expect both CREATION (from file creation) and PERMISSION (from chmod) From 4e15bc7e28d191f727a3da8d33c24690535f493f Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Wed, 11 Feb 2026 08:49:07 -0800 Subject: [PATCH 19/24] Apply suggestion from @Molter73 Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_file_open.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index b212cba2..839f2c6a 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -34,7 +34,7 @@ def test_open(fact, monitored_dir, server, filename): f.write('This is a test') # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD - fut_str = path_to_string(fut) + fut = path_to_string(fut) e = Event(process=Process.from_proc(), event_type=EventType.CREATION, file=fut_str, host_path='') From cffc8690552e37cc15989f1add418b98972c5c6c Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Wed, 11 Feb 2026 08:51:05 -0800 Subject: [PATCH 20/24] Apply suggestion from @Molter73 Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_chown.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 374af17c..4045509e 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -42,10 +42,8 @@ def test_chown(fact, test_container, server, filename): # Create the file and chown it # Use shlex.quote to properly escape special characters for shell fut_quoted = shlex.quote(fut) - touch_cmd_shell = f'touch {fut_quoted}' - chown_cmd_shell = f'chown {TEST_UID}:{TEST_GID} {fut_quoted}' - test_container.exec_run(touch_cmd_shell) - test_container.exec_run(chown_cmd_shell) + test_container.exec_run(f'touch {fut_quoted}') + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut_quoted}') # The args in the event won't have quotes (shell removes them) touch_cmd = f'touch {fut}' From 1ef880f10c18965b523df449b75bf24b87c45caa Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Wed, 11 Feb 2026 08:51:40 -0800 Subject: [PATCH 21/24] Apply suggestion from @Molter73 Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_unlink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 3e6395f5..2a977ea7 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -37,7 +37,7 @@ def test_remove(fact, monitored_dir, server, filename): os.remove(test_file) # Convert test_file to string for the Event, replacing invalid UTF-8 with U+FFFD - test_file_str = path_to_string(test_file) + test_file = path_to_string(test_file) process = Process.from_proc() # We expect both CREATION (from file creation) and UNLINK (from removal) From 361abb8232dd7437b5f1cd695e8a2d29e44c64ee Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 11 Feb 2026 09:10:27 -0800 Subject: [PATCH 22/24] Minor follow on changes for suggested changes --- tests/test_file_open.py | 2 +- tests/test_path_chmod.py | 4 ++-- tests/test_path_unlink.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index 839f2c6a..001aa4a3 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -37,7 +37,7 @@ def test_open(fact, monitored_dir, server, filename): fut = path_to_string(fut) e = Event(process=Process.from_proc(), event_type=EventType.CREATION, - file=fut_str, host_path='') + file=fut, host_path='') print(f'Waiting for event: {e}') server.wait_events([e]) diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index f9266b5b..b62391cc 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -42,9 +42,9 @@ def test_chmod(fact, monitored_dir, server, filename): # We expect both CREATION (from file creation) and PERMISSION (from chmod) events = [ Event(process=process, event_type=EventType.CREATION, - file=fut_str, host_path=''), + file=fut, host_path=''), Event(process=process, event_type=EventType.PERMISSION, - file=fut_str, host_path='', mode=mode), + file=fut, host_path='', mode=mode), ] for e in events: diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 2a977ea7..1249e618 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -43,9 +43,9 @@ def test_remove(fact, monitored_dir, server, filename): # We expect both CREATION (from file creation) and UNLINK (from removal) events = [ Event(process=process, event_type=EventType.CREATION, - file=test_file_str, host_path=''), + file=test_file, host_path=''), Event(process=process, event_type=EventType.UNLINK, - file=test_file_str, host_path=''), + file=test_file, host_path=''), ] server.wait_events(events) From e7c5c1b986d6894406710a778283b360bb11d462 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 11 Feb 2026 09:18:37 -0800 Subject: [PATCH 23/24] Minor suggested change to tests/test_path_chown.py --- tests/test_path_chown.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 4045509e..9e4daf12 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -33,11 +33,9 @@ def test_chown(fact, test_container, server, filename): server: The server instance to communicate with. filename: Name of the file to create (includes UTF-8 test cases). """ - # Convert filename to string, replacing invalid UTF-8 with U+FFFD - filename_str = path_to_string(filename) # File Under Test - fut = f'/container-dir/{filename_str}' + fut = f'/container-dir/{path_to_string(filename)}' # Create the file and chown it # Use shlex.quote to properly escape special characters for shell From dc43848f40492aeb1a4e6e182ffd586129a2f572 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 11 Feb 2026 09:45:17 -0800 Subject: [PATCH 24/24] Renamed test_file to fut in tests/test_path_unlink.py --- tests/test_path_unlink.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 1249e618..beafed0c 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -27,25 +27,27 @@ def test_remove(fact, monitored_dir, server, filename): server: The server instance to communicate with. filename: Name of the file to create and remove (includes UTF-8 test cases). """ - test_file = join_path_with_filename(monitored_dir, filename) + + # File under test + fut = join_path_with_filename(monitored_dir, filename) # Create the file first - with open(test_file, 'w') as f: + with open(fut, 'w') as f: f.write('This is a test') # Remove the file - os.remove(test_file) + os.remove(fut) # Convert test_file to string for the Event, replacing invalid UTF-8 with U+FFFD - test_file = path_to_string(test_file) + fut = path_to_string(fut) process = Process.from_proc() # We expect both CREATION (from file creation) and UNLINK (from removal) events = [ Event(process=process, event_type=EventType.CREATION, - file=test_file, host_path=''), + file=fut, host_path=''), Event(process=process, event_type=EventType.UNLINK, - file=test_file, host_path=''), + file=fut, host_path=''), ] server.wait_events(events)