From 25d3c808ea1138f27c717560ab17990eb1654aad Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 23 Apr 2026 10:56:54 -0700 Subject: [PATCH 01/15] X-Smart-Branch-Parent: main From 175ce52100d3bbfbc96b5044a093d21fcdeab444 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 16 Apr 2026 14:56:15 -0700 Subject: [PATCH 02/15] Added tests. Test check for both events and metrics. In the future there will only be metrics --- tests/test_path_rmdir.py | 337 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 tests/test_path_rmdir.py diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py new file mode 100644 index 00000000..0c2f28c2 --- /dev/null +++ b/tests/test_path_rmdir.py @@ -0,0 +1,337 @@ +import os +import shutil + +import pytest +import requests + +from event import Event, EventType, Process + + +def get_inode_removed_count(fact_config): + """ + Query Prometheus metrics to get the count of removed inodes. + + Args: + fact_config: The fact configuration tuple (config dict, config file path). + + Returns: + The current value of host_scanner_scan{label="inode_removed"} metric. + """ + config, _ = fact_config + response = requests.get(f'http://{config["endpoint"]["address"]}/metrics') + assert response.status_code == 200 + + for line in response.text.split('\n'): + if 'host_scanner_scan{label="inode_removed"}' in line: + # Format: host_scanner_scan{label="inode_removed"} 42 + return int(line.split()[-1]) + + return 0 + + +@pytest.mark.parametrize("dirname", [ + pytest.param('testdir', id='ASCII'), + pytest.param('café', id='French'), + pytest.param('файл', id='Cyrillic'), + pytest.param('日本語', id='Japanese'), +]) +def test_rmdir_empty(monitored_dir, server, fact_config, dirname): + """ + Tests that removing an empty directory properly cleans up inode tracking. + + Scenario: File is removed first, leaving an empty directory, then rmdir is called. + + For now, directory deletion events are reported (like file unlink). + Later, these events will be filtered out but inode cleanup will still happen. + + We use exact delta matching because: + - Each test has an isolated monitored_dir + - Periodic scans are disabled (scan_interval: 0) + - No background activity should interfere + + Args: + monitored_dir: Temporary directory path for creating the test directory. + server: The server instance to communicate with. + fact_config: The fact configuration. + dirname: Directory name to test (including UTF-8 variants). + """ + process = Process.from_proc() + + # Get baseline metric count + initial_count = get_inode_removed_count(fact_config) + + # Create a directory + test_dir = os.path.join(monitored_dir, dirname) + os.mkdir(test_dir) + + # Create a file in it + test_file = os.path.join(test_dir, 'file.txt') + with open(test_file, 'w') as f: + f.write('test content') + + # File creation should be tracked + e1 = Event(process=process, event_type=EventType.CREATION, + file=test_file, host_path=test_file) + + server.wait_events([e1]) + + # Remove the file first, leaving an empty directory + os.remove(test_file) + + # File deletion should be tracked + e2 = Event(process=process, event_type=EventType.UNLINK, + file=test_file, host_path=test_file) + + server.wait_events([e2]) + + # Check that file deletion incremented the metric by exactly 1 + count_after_file = get_inode_removed_count(fact_config) + file_delta = count_after_file - initial_count + assert file_delta == 1, \ + f"Expected exactly 1 inode removed for file deletion, got {file_delta}" + + # Now remove the empty directory with rmdir + os.rmdir(test_dir) + + # Directory deletion should be reported (TODO: this will be filtered out later) + e3 = Event(process=process, event_type=EventType.UNLINK, + file=test_dir, host_path=test_dir) + + server.wait_events([e3]) + + # Check that directory deletion also incremented the metric by exactly 1 + final_count = get_inode_removed_count(fact_config) + total_delta = final_count - initial_count + assert total_delta == 2, \ + f"Expected exactly 2 inodes removed (1 file + 1 dir), got {total_delta}" + + +def test_rmdir_tree(monitored_dir, server, fact_config): + """ + Tests that removing a directory tree recursively cleans up all inode tracking. + + Scenario: Directory with nested subdirectories and files is removed recursively + using shutil.rmtree (similar to rm -rf). + + This tests that all inodes (both files and directories) are properly removed + from tracking when a tree is deleted. + + Args: + monitored_dir: Temporary directory path for creating test directories. + server: The server instance to communicate with. + fact_config: The fact configuration. + """ + process = Process.from_proc() + + # Get baseline metric count + initial_count = get_inode_removed_count(fact_config) + + # Create nested directories + level1 = os.path.join(monitored_dir, 'level1') + level2 = os.path.join(level1, 'level2') + level3 = os.path.join(level2, 'level3') + os.makedirs(level3) + + # Create files at different levels + file1 = os.path.join(level1, 'file1.txt') + file2 = os.path.join(level2, 'file2.txt') + file3 = os.path.join(level3, 'file3.txt') + + with open(file1, 'w') as f: + f.write('level1') + with open(file2, 'w') as f: + f.write('level2') + with open(file3, 'w') as f: + f.write('level3') + + # All files should be tracked + creation_events = [ + Event(process=process, event_type=EventType.CREATION, + file=file1, host_path=file1), + Event(process=process, event_type=EventType.CREATION, + file=file2, host_path=file2), + Event(process=process, event_type=EventType.CREATION, + file=file3, host_path=file3), + ] + + server.wait_events(creation_events) + + # Remove the entire tree recursively (like rm -rf) + # This will generate events for all files and directories + shutil.rmtree(level1) + + # All deletions should be tracked: 3 files + 3 directories + # (level1, level2, level3) + unlink_events = [ + Event(process=process, event_type=EventType.UNLINK, + file=file1, host_path=file1), + Event(process=process, event_type=EventType.UNLINK, + file=file2, host_path=file2), + Event(process=process, event_type=EventType.UNLINK, + file=file3, host_path=file3), + Event(process=process, event_type=EventType.UNLINK, + file=level1, host_path=level1), + Event(process=process, event_type=EventType.UNLINK, + file=level2, host_path=level2), + Event(process=process, event_type=EventType.UNLINK, + file=level3, host_path=level3), + ] + + server.wait_events(unlink_events) + + # Check that all inodes were removed: 3 files + 3 directories = 6 total + final_count = get_inode_removed_count(fact_config) + total_delta = final_count - initial_count + assert total_delta == 6, \ + f"Expected exactly 6 inodes removed (3 files + 3 dirs), got {total_delta}" + + +def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): + """ + Tests that directories removed outside monitored paths don't affect tracking. + + Verifies that inode_removed metric only increments for monitored paths. + + Args: + monitored_dir: Temporary directory path that is monitored. + ignored_dir: Temporary directory path that is not monitored. + server: The server instance to communicate with. + fact_config: The fact configuration. + """ + process = Process.from_proc() + + # Get baseline metric count + initial_count = get_inode_removed_count(fact_config) + + # Create directory in ignored path + ignored_subdir = os.path.join(ignored_dir, 'ignored_subdir') + os.mkdir(ignored_subdir) + ignored_file = os.path.join(ignored_subdir, 'ignored.txt') + with open(ignored_file, 'w') as f: + f.write('ignored') + + # Remove ignored file and directory - should NOT generate events or increment metrics + os.remove(ignored_file) + os.rmdir(ignored_subdir) + + # Metric should not have changed + count_after_ignored = get_inode_removed_count(fact_config) + assert count_after_ignored == initial_count, \ + f"Ignored path operations should not increment inode_removed metric" + + # Create and remove directory in monitored path + monitored_subdir = os.path.join(monitored_dir, 'monitored_subdir') + os.mkdir(monitored_subdir) + monitored_file = os.path.join(monitored_subdir, 'monitored.txt') + with open(monitored_file, 'w') as f: + f.write('monitored') + + # Monitored file creation should generate an event + e1 = Event(process=process, event_type=EventType.CREATION, + file=monitored_file, host_path=monitored_file) + + server.wait_events([e1]) + + # Remove monitored file and directory + os.remove(monitored_file) + os.rmdir(monitored_subdir) + + # Both deletions should be tracked + deletion_events = [ + Event(process=process, event_type=EventType.UNLINK, + file=monitored_file, host_path=monitored_file), + Event(process=process, event_type=EventType.UNLINK, + file=monitored_subdir, host_path=monitored_subdir), + ] + + server.wait_events(deletion_events) + + # Metric should have incremented by exactly 2 (file + dir) + final_count = get_inode_removed_count(fact_config) + total_delta = final_count - initial_count + assert total_delta == 2, \ + f"Expected exactly 2 inodes removed from monitored path, got {total_delta}" + + +def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): + """ + Tests that directory deletion properly handles parent inode relationships. + + This is important because after deleting a subdirectory, the parent directory + should still be tracked and able to track new files created in it. + + Args: + monitored_dir: Temporary directory path for creating test directories. + server: The server instance to communicate with. + fact_config: The fact configuration. + """ + process = Process.from_proc() + + # Get baseline metric count + initial_count = get_inode_removed_count(fact_config) + + # Create a subdirectory + subdir = os.path.join(monitored_dir, 'subdir') + os.mkdir(subdir) + + # Create a file in the subdirectory + test_file = os.path.join(subdir, 'test.txt') + with open(test_file, 'w') as f: + f.write('content') + + # Verify file creation is tracked + e1 = Event(process=process, event_type=EventType.CREATION, + file=test_file, host_path=test_file) + server.wait_events([e1]) + + # Create another file at the root level (parent directory) + root_file = os.path.join(monitored_dir, 'root.txt') + with open(root_file, 'w') as f: + f.write('root content') + + e2 = Event(process=process, event_type=EventType.CREATION, + file=root_file, host_path=root_file) + server.wait_events([e2]) + + # Remove the subdirectory and its contents + os.remove(test_file) + os.rmdir(subdir) + + # Verify deletions are tracked + deletion_events = [ + Event(process=process, event_type=EventType.UNLINK, + file=test_file, host_path=test_file), + Event(process=process, event_type=EventType.UNLINK, + file=subdir, host_path=subdir), + ] + server.wait_events(deletion_events) + + # Check metric incremented by 2 (file + subdir) + count_after_subdir = get_inode_removed_count(fact_config) + delta_after_subdir = count_after_subdir - initial_count + assert delta_after_subdir == 2, \ + f"Expected 2 inodes removed (file + subdir), got {delta_after_subdir}" + + # Create a NEW file in the parent directory (monitored_dir) + # This tests that removing the subdirectory didn't corrupt + # the parent directory's inode tracking + new_file = os.path.join(monitored_dir, 'new.txt') + with open(new_file, 'w') as f: + f.write('new content') + + e4 = Event(process=process, event_type=EventType.CREATION, + file=new_file, host_path=new_file) + server.wait_events([e4]) + + # Remove the new file to clean up + os.remove(new_file) + + e5 = Event(process=process, event_type=EventType.UNLINK, + file=new_file, host_path=new_file) + server.wait_events([e5]) + + # Final metric check: should be 3 total (test_file, subdir, new_file) + final_count = get_inode_removed_count(fact_config) + total_delta = final_count - initial_count + assert total_delta == 3, \ + f"Expected 3 inodes removed total, got {total_delta}" From dc913bdaa56ce39d55cc342805199a992f6706de Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 16 Apr 2026 16:47:40 -0700 Subject: [PATCH 03/15] Tests pass now --- fact-ebpf/src/bpf/events.h | 13 ++++++++++++ fact-ebpf/src/bpf/main.c | 34 ++++++++++++++++++++++++++++++ fact-ebpf/src/bpf/types.h | 2 ++ fact-ebpf/src/lib.rs | 1 + fact/src/event/mod.rs | 21 +++++++++++++++++- fact/src/metrics/kernel_metrics.rs | 9 ++++++++ tests/test_path_rmdir.py | 17 +++++++++------ 7 files changed, 89 insertions(+), 8 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 276a1d97..81307888 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -131,3 +131,16 @@ __always_inline static void submit_mkdir_event(struct submit_event_args_t* args) // d_instantiate doesn't support bpf_d_path, so we use false and rely on the stashed path from path_mkdir __submit_event(args, false); } + +__always_inline static void submit_rmdir_event(struct metrics_by_hook_t* m, + const char filename[PATH_MAX], + inode_key_t* inode, + inode_key_t* parent_inode) { + struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); + if (event == NULL) { + m->ringbuffer_full++; + return; + } + + __submit_event(event, m, DIR_ACTIVITY_UNLINK, filename, inode, parent_inode, path_hooks_support_bpf_d_path); +} diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 4b682040..b1f9300f 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -326,3 +326,37 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { bpf_map_delete_elem(&mkdir_context, &pid_tgid); return 0; } + +SEC("lsm/path_rmdir") +int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->path_rmdir.total++; + + struct bound_path_t* path = path_read_append_d_entry(dir, dentry); + if (path == NULL) { + bpf_printk("Failed to read path"); + m->path_rmdir.error++; + return 0; + } + + inode_key_t inode_key = inode_to_key(dentry->d_inode); + inode_key_t* inode_to_submit = &inode_key; + + if (is_monitored(inode_key, path, NULL, &inode_to_submit) == NOT_MONITORED) { + m->path_rmdir.ignored++; + return 0; + } + + // Remove directory inode from tracking + inode_remove(&inode_key); + + submit_rmdir_event(&m->path_rmdir, + path->path, + inode_to_submit, + NULL); + return 0; +} diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 95c67e86..9a395da4 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -56,6 +56,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_CHOWN, FILE_ACTIVITY_RENAME, DIR_ACTIVITY_CREATION, + DIR_ACTIVITY_UNLINK, } file_activity_type_t; struct event_t { @@ -120,4 +121,5 @@ struct metrics_t { struct metrics_by_hook_t path_rename; struct metrics_by_hook_t path_mkdir; struct metrics_by_hook_t d_instantiate; + struct metrics_by_hook_t path_rmdir; }; diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index c4c95ec6..ba84fb01 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -126,6 +126,7 @@ impl metrics_t { m.path_chown = m.path_chown.accumulate(&other.path_chown); m.path_rename = m.path_rename.accumulate(&other.path_rename); m.path_mkdir = m.path_mkdir.accumulate(&other.path_mkdir); + m.path_rmdir = m.path_rmdir.accumulate(&other.path_rmdir); m.d_instantiate = m.d_instantiate.accumulate(&other.d_instantiate); m } diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 1c87a859..7f526d40 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -134,8 +134,12 @@ impl Event { matches!(self.file, FileData::MkDir(_)) } + pub fn is_rmdir(&self) -> bool { + matches!(self.file, FileData::RmDir(_)) + } + pub fn is_unlink(&self) -> bool { - matches!(self.file, FileData::Unlink(_)) + matches!(self.file, FileData::Unlink(_) | FileData::RmDir(_)) } /// Unwrap the inner FileData and return the inode that triggered @@ -148,6 +152,7 @@ impl Event { FileData::Open(data) => &data.inode, FileData::Creation(data) => &data.inode, FileData::MkDir(data) => &data.inode, + FileData::RmDir(data) => &data.inode, FileData::Unlink(data) => &data.inode, FileData::Chmod(data) => &data.inner.inode, FileData::Chown(data) => &data.inner.inode, @@ -161,6 +166,7 @@ impl Event { FileData::Open(data) => &data.parent_inode, FileData::Creation(data) => &data.parent_inode, FileData::MkDir(data) => &data.parent_inode, + FileData::RmDir(data) => &data.parent_inode, FileData::Unlink(data) => &data.parent_inode, FileData::Chmod(data) => &data.inner.parent_inode, FileData::Chown(data) => &data.inner.parent_inode, @@ -183,6 +189,7 @@ impl Event { FileData::Open(data) => &data.filename, FileData::Creation(data) => &data.filename, FileData::MkDir(data) => &data.filename, + FileData::RmDir(data) => &data.filename, FileData::Unlink(data) => &data.filename, FileData::Chmod(data) => &data.inner.filename, FileData::Chown(data) => &data.inner.filename, @@ -202,6 +209,7 @@ impl Event { FileData::Open(data) => &data.host_file, FileData::Creation(data) => &data.host_file, FileData::MkDir(data) => &data.host_file, + FileData::RmDir(data) => &data.host_file, FileData::Unlink(data) => &data.host_file, FileData::Chmod(data) => &data.inner.host_file, FileData::Chown(data) => &data.inner.host_file, @@ -218,6 +226,7 @@ impl Event { FileData::Open(data) => data.host_file = host_path, FileData::Creation(data) => data.host_file = host_path, FileData::MkDir(data) => data.host_file = host_path, + FileData::RmDir(data) => data.host_file = host_path, FileData::Unlink(data) => data.host_file = host_path, FileData::Chmod(data) => data.inner.host_file = host_path, FileData::Chown(data) => data.inner.host_file = host_path, @@ -303,6 +312,7 @@ pub enum FileData { Open(BaseFileData), Creation(BaseFileData), MkDir(BaseFileData), + RmDir(BaseFileData), Unlink(BaseFileData), Chmod(ChmodFileData), Chown(ChownFileData), @@ -322,6 +332,7 @@ impl FileData { file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner), file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::MkDir(inner), + file_activity_type_t::DIR_ACTIVITY_UNLINK => FileData::RmDir(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_CHMOD => { let data = ChmodFileData { @@ -373,6 +384,13 @@ impl From for fact_api::file_activity::File { FileData::MkDir(_) => { unreachable!("MkDir event reached protobuf conversion"); } + FileData::RmDir(event) => { + // For now, report directory deletion as unlink + // TODO: Filter this out like MkDir once inode tracking is stable + let activity = Some(fact_api::FileActivityBase::from(event)); + let f_act = fact_api::FileUnlink { activity }; + fact_api::file_activity::File::Unlink(f_act) + } FileData::Unlink(event) => { let activity = Some(fact_api::FileActivityBase::from(event)); let f_act = fact_api::FileUnlink { activity }; @@ -401,6 +419,7 @@ impl PartialEq for FileData { (FileData::Open(this), FileData::Open(other)) => this == other, (FileData::Creation(this), FileData::Creation(other)) => this == other, (FileData::MkDir(this), FileData::MkDir(other)) => this == other, + (FileData::RmDir(this), FileData::RmDir(other)) => this == other, (FileData::Unlink(this), FileData::Unlink(other)) => this == other, (FileData::Chmod(this), FileData::Chmod(other)) => this == other, (FileData::Rename(this), FileData::Rename(other)) => this == other, diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 9caa1ff3..15da3993 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -14,6 +14,7 @@ pub struct KernelMetrics { path_chown: EventCounter, path_rename: EventCounter, path_mkdir: EventCounter, + path_rmdir: EventCounter, d_instantiate: EventCounter, map: PerCpuArray, } @@ -50,6 +51,11 @@ impl KernelMetrics { "Events processed by the path_mkdir LSM hook", &[], // Labels are not needed since `collect` will add them all ); + let path_rmdir = EventCounter::new( + "kernel_path_rmdir_events", + "Events processed by the path_rmdir LSM hook", + &[], // Labels are not needed since `collect` will add them all + ); let d_instantiate = EventCounter::new( "kernel_d_instantiate_events", "Events processed by the d_instantiate LSM hook", @@ -62,6 +68,7 @@ impl KernelMetrics { path_chown.register(reg); path_rename.register(reg); path_mkdir.register(reg); + path_rmdir.register(reg); d_instantiate.register(reg); KernelMetrics { @@ -71,6 +78,7 @@ impl KernelMetrics { path_chown, path_rename, path_mkdir, + path_rmdir, d_instantiate, map: kernel_metrics, } @@ -122,6 +130,7 @@ impl KernelMetrics { KernelMetrics::refresh_labels(&self.path_chown, &metrics.path_chown); KernelMetrics::refresh_labels(&self.path_rename, &metrics.path_rename); KernelMetrics::refresh_labels(&self.path_mkdir, &metrics.path_mkdir); + KernelMetrics::refresh_labels(&self.path_rmdir, &metrics.path_rmdir); KernelMetrics::refresh_labels(&self.d_instantiate, &metrics.d_instantiate); Ok(()) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index 0c2f28c2..4c8e0d76 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -15,16 +15,18 @@ def get_inode_removed_count(fact_config): fact_config: The fact configuration tuple (config dict, config file path). Returns: - The current value of host_scanner_scan{label="inode_removed"} metric. + The current value of host_scanner_scan{label="InodeRemoved"} metric. """ config, _ = fact_config response = requests.get(f'http://{config["endpoint"]["address"]}/metrics') assert response.status_code == 200 for line in response.text.split('\n'): - if 'host_scanner_scan{label="inode_removed"}' in line: - # Format: host_scanner_scan{label="inode_removed"} 42 - return int(line.split()[-1]) + if 'host_scanner_scan' in line and 'label="InodeRemoved"' in line: + # Format: host_scanner_scan{label="InodeRemoved"} 42 + parts = line.split() + if len(parts) >= 2: + return int(parts[-1]) return 0 @@ -158,10 +160,11 @@ def test_rmdir_tree(monitored_dir, server, fact_config): # Remove the entire tree recursively (like rm -rf) # This will generate events for all files and directories + # Order: deepest files/dirs first, then work up to the root shutil.rmtree(level1) # All deletions should be tracked: 3 files + 3 directories - # (level1, level2, level3) + # shutil.rmtree deletes depth-first: file1, file2, file3, level3, level2, level1 unlink_events = [ Event(process=process, event_type=EventType.UNLINK, file=file1, host_path=file1), @@ -170,11 +173,11 @@ def test_rmdir_tree(monitored_dir, server, fact_config): Event(process=process, event_type=EventType.UNLINK, file=file3, host_path=file3), Event(process=process, event_type=EventType.UNLINK, - file=level1, host_path=level1), + file=level3, host_path=level3), Event(process=process, event_type=EventType.UNLINK, file=level2, host_path=level2), Event(process=process, event_type=EventType.UNLINK, - file=level3, host_path=level3), + file=level1, host_path=level1), ] server.wait_events(unlink_events) From 3c0ce4a9dc9cc2544cf8deea241b5e25dcb7a3a6 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 17 Apr 2026 09:39:08 -0700 Subject: [PATCH 04/15] Minor change to logging --- fact-ebpf/src/bpf/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index b1f9300f..b6ec78fe 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -338,7 +338,7 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { struct bound_path_t* path = path_read_append_d_entry(dir, dentry); if (path == NULL) { - bpf_printk("Failed to read path"); + bpf_printk("Failed to directory read path"); m->path_rmdir.error++; return 0; } From 8fa324849279fc0de2dfb2019574fa5f0a287d7f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 17 Apr 2026 11:40:29 -0700 Subject: [PATCH 05/15] Added get_metric_value helper function --- tests/test_path_rmdir.py | 16 +++------------- tests/utils.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index 4c8e0d76..c9d0c90e 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -2,9 +2,9 @@ import shutil import pytest -import requests from event import Event, EventType, Process +from utils import get_metric_value def get_inode_removed_count(fact_config): @@ -17,18 +17,8 @@ def get_inode_removed_count(fact_config): Returns: The current value of host_scanner_scan{label="InodeRemoved"} metric. """ - config, _ = fact_config - response = requests.get(f'http://{config["endpoint"]["address"]}/metrics') - assert response.status_code == 200 - - for line in response.text.split('\n'): - if 'host_scanner_scan' in line and 'label="InodeRemoved"' in line: - # Format: host_scanner_scan{label="InodeRemoved"} 42 - parts = line.split() - if len(parts) >= 2: - return int(parts[-1]) - - return 0 + value = get_metric_value(fact_config, "host_scanner_scan", {"label": "InodeRemoved"}) + return int(value) if value is not None else 0 @pytest.mark.parametrize("dirname", [ diff --git a/tests/utils.py b/tests/utils.py index b99254d9..684632fc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,8 @@ import os import re +import requests + def join_path_with_filename(directory, filename): """ @@ -66,8 +68,40 @@ def rust_style_join(args): """ Concatenate arguments after quoting them. Each argument is separated by a single space. - + Args: args: The string to quote """ return ' '.join(rust_style_quote(arg) for arg in args) + + +def get_metric_value(fact_config, metric_name, labels=None): + """ + Query Prometheus metrics endpoint to get the value of a metric. + + Args: + fact_config: The fact configuration tuple (config dict, config file path). + metric_name: Name of the metric to query (e.g., "host_scanner_scan"). + labels: Optional dict of label filters (e.g., {"label": "InodeRemoved"}). + + Returns: + The metric value as a string if found, None otherwise. + """ + config, _ = fact_config + response = requests.get(f'http://{config["endpoint"]["address"]}/metrics') + assert response.status_code == 200 + + labels = labels or {} + + for line in response.text.split('\n'): + if metric_name not in line: + continue + + # Check if all label filters match + if all(f'{k}="{v}"' in line for k, v in labels.items()): + # Format: metric_name{label="value"} 42 + parts = line.split() + if len(parts) >= 2: + return parts[-1] + + return None From 51905f9b135a46b947b58bde55d1c516e3158264 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 09:57:04 -0700 Subject: [PATCH 06/15] Checking kernel_path_rmdir_events metrics --- tests/test_path_rmdir.py | 131 ++++++++++++++++++++++++++++----------- 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index c9d0c90e..04aef367 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -21,6 +21,25 @@ def get_inode_removed_count(fact_config): return int(value) if value is not None else 0 +def get_kernel_rmdir_processed(fact_config): + """ + Query Prometheus metrics to get the count of processed (non-ignored) rmdir events. + + Args: + fact_config: The fact configuration tuple (config dict, config file path). + + Returns: + The difference between Total and Ignored kernel_path_rmdir_events. + """ + total_str = get_metric_value(fact_config, "kernel_path_rmdir_events", {"label": "Total"}) + ignored_str = get_metric_value(fact_config, "kernel_path_rmdir_events", {"label": "Ignored"}) + + total = int(total_str) if total_str is not None else 0 + ignored = int(ignored_str) if ignored_str is not None else 0 + + return total - ignored + + @pytest.mark.parametrize("dirname", [ pytest.param('testdir', id='ASCII'), pytest.param('café', id='French'), @@ -49,8 +68,9 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): """ process = Process.from_proc() - # Get baseline metric count - initial_count = get_inode_removed_count(fact_config) + # Get baseline metric counts + initial_inode_removed = get_inode_removed_count(fact_config) + initial_kernel_rmdir = get_kernel_rmdir_processed(fact_config) # Create a directory test_dir = os.path.join(monitored_dir, dirname) @@ -78,7 +98,7 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): # Check that file deletion incremented the metric by exactly 1 count_after_file = get_inode_removed_count(fact_config) - file_delta = count_after_file - initial_count + file_delta = count_after_file - initial_inode_removed assert file_delta == 1, \ f"Expected exactly 1 inode removed for file deletion, got {file_delta}" @@ -91,11 +111,17 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): server.wait_events([e3]) - # Check that directory deletion also incremented the metric by exactly 1 - final_count = get_inode_removed_count(fact_config) - total_delta = final_count - initial_count - assert total_delta == 2, \ - f"Expected exactly 2 inodes removed (1 file + 1 dir), got {total_delta}" + # Check that directory deletion incremented both metrics by exactly 1 + final_inode_removed = get_inode_removed_count(fact_config) + final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) + + inode_delta = final_inode_removed - initial_inode_removed + kernel_delta = final_kernel_rmdir - initial_kernel_rmdir + + assert inode_delta == 2, \ + f"Expected exactly 2 inodes removed (1 file + 1 dir), got {inode_delta}" + assert kernel_delta == 1, \ + f"Expected exactly 1 kernel rmdir event processed, got {kernel_delta}" def test_rmdir_tree(monitored_dir, server, fact_config): @@ -115,8 +141,9 @@ def test_rmdir_tree(monitored_dir, server, fact_config): """ process = Process.from_proc() - # Get baseline metric count - initial_count = get_inode_removed_count(fact_config) + # Get baseline metric counts + initial_inode_removed = get_inode_removed_count(fact_config) + initial_kernel_rmdir = get_kernel_rmdir_processed(fact_config) # Create nested directories level1 = os.path.join(monitored_dir, 'level1') @@ -172,11 +199,17 @@ def test_rmdir_tree(monitored_dir, server, fact_config): server.wait_events(unlink_events) - # Check that all inodes were removed: 3 files + 3 directories = 6 total - final_count = get_inode_removed_count(fact_config) - total_delta = final_count - initial_count - assert total_delta == 6, \ - f"Expected exactly 6 inodes removed (3 files + 3 dirs), got {total_delta}" + # Check that all inodes and kernel events were tracked + final_inode_removed = get_inode_removed_count(fact_config) + final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) + + inode_delta = final_inode_removed - initial_inode_removed + kernel_delta = final_kernel_rmdir - initial_kernel_rmdir + + assert inode_delta == 6, \ + f"Expected exactly 6 inodes removed (3 files + 3 dirs), got {inode_delta}" + assert kernel_delta == 3, \ + f"Expected exactly 3 kernel rmdir events processed, got {kernel_delta}" def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): @@ -193,8 +226,9 @@ def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): """ process = Process.from_proc() - # Get baseline metric count - initial_count = get_inode_removed_count(fact_config) + # Get baseline metric counts + initial_inode_removed = get_inode_removed_count(fact_config) + initial_kernel_rmdir = get_kernel_rmdir_processed(fact_config) # Create directory in ignored path ignored_subdir = os.path.join(ignored_dir, 'ignored_subdir') @@ -207,10 +241,13 @@ def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): os.remove(ignored_file) os.rmdir(ignored_subdir) - # Metric should not have changed - count_after_ignored = get_inode_removed_count(fact_config) - assert count_after_ignored == initial_count, \ + # Metrics should not have changed + inode_after_ignored = get_inode_removed_count(fact_config) + kernel_after_ignored = get_kernel_rmdir_processed(fact_config) + assert inode_after_ignored == initial_inode_removed, \ f"Ignored path operations should not increment inode_removed metric" + assert kernel_after_ignored == initial_kernel_rmdir, \ + f"Ignored path operations should not increment kernel_rmdir_processed metric" # Create and remove directory in monitored path monitored_subdir = os.path.join(monitored_dir, 'monitored_subdir') @@ -239,11 +276,17 @@ def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): server.wait_events(deletion_events) - # Metric should have incremented by exactly 2 (file + dir) - final_count = get_inode_removed_count(fact_config) - total_delta = final_count - initial_count - assert total_delta == 2, \ - f"Expected exactly 2 inodes removed from monitored path, got {total_delta}" + # Metrics should have incremented by exactly 2 inodes and 1 kernel rmdir + final_inode_removed = get_inode_removed_count(fact_config) + final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) + + inode_delta = final_inode_removed - initial_inode_removed + kernel_delta = final_kernel_rmdir - initial_kernel_rmdir + + assert inode_delta == 2, \ + f"Expected exactly 2 inodes removed from monitored path, got {inode_delta}" + assert kernel_delta == 1, \ + f"Expected exactly 1 kernel rmdir event processed, got {kernel_delta}" def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): @@ -260,8 +303,9 @@ def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): """ process = Process.from_proc() - # Get baseline metric count - initial_count = get_inode_removed_count(fact_config) + # Get baseline metric counts + initial_inode_removed = get_inode_removed_count(fact_config) + initial_kernel_rmdir = get_kernel_rmdir_processed(fact_config) # Create a subdirectory subdir = os.path.join(monitored_dir, 'subdir') @@ -299,11 +343,17 @@ def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): ] server.wait_events(deletion_events) - # Check metric incremented by 2 (file + subdir) - count_after_subdir = get_inode_removed_count(fact_config) - delta_after_subdir = count_after_subdir - initial_count - assert delta_after_subdir == 2, \ - f"Expected 2 inodes removed (file + subdir), got {delta_after_subdir}" + # Check metrics incremented (file + subdir) + inode_after_subdir = get_inode_removed_count(fact_config) + kernel_after_subdir = get_kernel_rmdir_processed(fact_config) + + inode_delta_subdir = inode_after_subdir - initial_inode_removed + kernel_delta_subdir = kernel_after_subdir - initial_kernel_rmdir + + assert inode_delta_subdir == 2, \ + f"Expected 2 inodes removed (file + subdir), got {inode_delta_subdir}" + assert kernel_delta_subdir == 1, \ + f"Expected 1 kernel rmdir event processed, got {kernel_delta_subdir}" # Create a NEW file in the parent directory (monitored_dir) # This tests that removing the subdirectory didn't corrupt @@ -323,8 +373,15 @@ def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): file=new_file, host_path=new_file) server.wait_events([e5]) - # Final metric check: should be 3 total (test_file, subdir, new_file) - final_count = get_inode_removed_count(fact_config) - total_delta = final_count - initial_count - assert total_delta == 3, \ - f"Expected 3 inodes removed total, got {total_delta}" + # Final metric check: should be 3 total inodes (test_file, subdir, new_file) + # and 1 total kernel rmdir (subdir) + final_inode_removed = get_inode_removed_count(fact_config) + final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) + + inode_total_delta = final_inode_removed - initial_inode_removed + kernel_total_delta = final_kernel_rmdir - initial_kernel_rmdir + + assert inode_total_delta == 3, \ + f"Expected 3 inodes removed total, got {inode_total_delta}" + assert kernel_total_delta == 1, \ + f"Expected 1 kernel rmdir event total, got {kernel_total_delta}" From 78a3c2eced965ce4bc1e914609aad222afd83a12 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 10:48:11 -0700 Subject: [PATCH 07/15] Using rm instead shutils.rmtree --- tests/test_path_rmdir.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index 04aef367..6f0badaf 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -1,5 +1,6 @@ import os import shutil +import subprocess import pytest @@ -124,15 +125,17 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): f"Expected exactly 1 kernel rmdir event processed, got {kernel_delta}" -def test_rmdir_tree(monitored_dir, server, fact_config): +def test_rmdir_recursive_with_rm(monitored_dir, server, fact_config): """ Tests that removing a directory tree recursively cleans up all inode tracking. Scenario: Directory with nested subdirectories and files is removed recursively - using shutil.rmtree (similar to rm -rf). + using the rm -rf command via subprocess. This tests that all inodes (both files and directories) are properly removed - from tracking when a tree is deleted. + from tracking when a tree is deleted by an external process. The rm command + deletes directories eagerly (immediately after they become empty), creating + an interleaved deletion pattern. Args: monitored_dir: Temporary directory path for creating test directories. @@ -175,25 +178,34 @@ def test_rmdir_tree(monitored_dir, server, fact_config): server.wait_events(creation_events) - # Remove the entire tree recursively (like rm -rf) + # Remove the entire tree recursively using subprocess (like running rm -rf) # This will generate events for all files and directories # Order: deepest files/dirs first, then work up to the root - shutil.rmtree(level1) + proc = subprocess.Popen(["rm", "-rf", level1]) + + # Capture process info while subprocess is running + rm_process = Process.from_proc(proc.pid) + + # Wait for completion + proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"rm command failed with exit code {proc.returncode}") # All deletions should be tracked: 3 files + 3 directories - # shutil.rmtree deletes depth-first: file1, file2, file3, level3, level2, level1 + # rm -rf deletes each directory immediately after it empties (interleaved): + # file3, level3 (now empty), file2, level2 (now empty), file1, level1 (now empty) unlink_events = [ - Event(process=process, event_type=EventType.UNLINK, - file=file1, host_path=file1), - Event(process=process, event_type=EventType.UNLINK, - file=file2, host_path=file2), - Event(process=process, event_type=EventType.UNLINK, + Event(process=rm_process, event_type=EventType.UNLINK, file=file3, host_path=file3), - Event(process=process, event_type=EventType.UNLINK, + Event(process=rm_process, event_type=EventType.UNLINK, file=level3, host_path=level3), - Event(process=process, event_type=EventType.UNLINK, + Event(process=rm_process, event_type=EventType.UNLINK, + file=file2, host_path=file2), + Event(process=rm_process, event_type=EventType.UNLINK, file=level2, host_path=level2), - Event(process=process, event_type=EventType.UNLINK, + Event(process=rm_process, event_type=EventType.UNLINK, + file=file1, host_path=file1), + Event(process=rm_process, event_type=EventType.UNLINK, file=level1, host_path=level1), ] From 1167392cdb0f46b98e9b1a315de50662310e1bfd Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 11:18:24 -0700 Subject: [PATCH 08/15] Not sending directory deletion events --- fact/src/event/mod.rs | 8 ++------ fact/src/host_scanner.rs | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 7f526d40..1b346548 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -384,12 +384,8 @@ impl From for fact_api::file_activity::File { FileData::MkDir(_) => { unreachable!("MkDir event reached protobuf conversion"); } - FileData::RmDir(event) => { - // For now, report directory deletion as unlink - // TODO: Filter this out like MkDir once inode tracking is stable - let activity = Some(fact_api::FileActivityBase::from(event)); - let f_act = fact_api::FileUnlink { activity }; - fact_api::file_activity::File::Unlink(f_act) + FileData::RmDir(_) => { + unreachable!("RmDir event reached protobuf conversion"); } FileData::Unlink(event) => { let activity = Some(fact_api::FileActivityBase::from(event)); diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 7c9b5a81..075cc870 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -295,8 +295,8 @@ impl HostScanner { self.handle_unlink_event(&event); } - // Skip directory creation events - we track them internally but don't send to sensor - if event.is_mkdir() { + // Skip directory creation and deletion events - we track them internally but don't send to sensor + if event.is_mkdir() || event.is_rmdir() { continue; } From f2aa0fcb7ffc79650a1acb8b3cee34e657fe6a24 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 11:47:26 -0700 Subject: [PATCH 09/15] Tests do not expect directory deletion events --- tests/test_path_rmdir.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index 6f0badaf..04868e19 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -104,14 +104,9 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): f"Expected exactly 1 inode removed for file deletion, got {file_delta}" # Now remove the empty directory with rmdir + # Note: Directory deletions are tracked internally but not sent as events to sensors os.rmdir(test_dir) - # Directory deletion should be reported (TODO: this will be filtered out later) - e3 = Event(process=process, event_type=EventType.UNLINK, - file=test_dir, host_path=test_dir) - - server.wait_events([e3]) - # Check that directory deletion incremented both metrics by exactly 1 final_inode_removed = get_inode_removed_count(fact_config) final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) @@ -191,22 +186,16 @@ def test_rmdir_recursive_with_rm(monitored_dir, server, fact_config): if proc.returncode != 0: raise RuntimeError(f"rm command failed with exit code {proc.returncode}") - # All deletions should be tracked: 3 files + 3 directories - # rm -rf deletes each directory immediately after it empties (interleaved): - # file3, level3 (now empty), file2, level2 (now empty), file1, level1 (now empty) + # Only file deletions are reported as events + # Directory deletions are tracked internally but not sent to sensors + # rm -rf deletes depth-first: file3, file2, file1 unlink_events = [ Event(process=rm_process, event_type=EventType.UNLINK, file=file3, host_path=file3), - Event(process=rm_process, event_type=EventType.UNLINK, - file=level3, host_path=level3), Event(process=rm_process, event_type=EventType.UNLINK, file=file2, host_path=file2), - Event(process=rm_process, event_type=EventType.UNLINK, - file=level2, host_path=level2), Event(process=rm_process, event_type=EventType.UNLINK, file=file1, host_path=file1), - Event(process=rm_process, event_type=EventType.UNLINK, - file=level1, host_path=level1), ] server.wait_events(unlink_events) @@ -278,12 +267,11 @@ def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): os.remove(monitored_file) os.rmdir(monitored_subdir) - # Both deletions should be tracked + # Only file deletion is reported as an event + # Directory deletions are tracked internally but not sent to sensors deletion_events = [ Event(process=process, event_type=EventType.UNLINK, file=monitored_file, host_path=monitored_file), - Event(process=process, event_type=EventType.UNLINK, - file=monitored_subdir, host_path=monitored_subdir), ] server.wait_events(deletion_events) @@ -346,12 +334,11 @@ def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): os.remove(test_file) os.rmdir(subdir) - # Verify deletions are tracked + # Verify file deletion is tracked + # Directory deletions are tracked internally but not sent to sensors deletion_events = [ Event(process=process, event_type=EventType.UNLINK, file=test_file, host_path=test_file), - Event(process=process, event_type=EventType.UNLINK, - file=subdir, host_path=subdir), ] server.wait_events(deletion_events) From faf1bb5ebc68c5bdc57856bd43cc4699a03cd780 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 11:58:29 -0700 Subject: [PATCH 10/15] Cleaned up some comments --- tests/test_path_rmdir.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/test_path_rmdir.py b/tests/test_path_rmdir.py index 04868e19..3fce265f 100644 --- a/tests/test_path_rmdir.py +++ b/tests/test_path_rmdir.py @@ -53,9 +53,6 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): Scenario: File is removed first, leaving an empty directory, then rmdir is called. - For now, directory deletion events are reported (like file unlink). - Later, these events will be filtered out but inode cleanup will still happen. - We use exact delta matching because: - Each test has an isolated monitored_dir - Periodic scans are disabled (scan_interval: 0) @@ -104,10 +101,9 @@ def test_rmdir_empty(monitored_dir, server, fact_config, dirname): f"Expected exactly 1 inode removed for file deletion, got {file_delta}" # Now remove the empty directory with rmdir - # Note: Directory deletions are tracked internally but not sent as events to sensors os.rmdir(test_dir) - # Check that directory deletion incremented both metrics by exactly 1 + # Check metrics after directory deletion final_inode_removed = get_inode_removed_count(fact_config) final_kernel_rmdir = get_kernel_rmdir_processed(fact_config) @@ -186,9 +182,7 @@ def test_rmdir_recursive_with_rm(monitored_dir, server, fact_config): if proc.returncode != 0: raise RuntimeError(f"rm command failed with exit code {proc.returncode}") - # Only file deletions are reported as events - # Directory deletions are tracked internally but not sent to sensors - # rm -rf deletes depth-first: file3, file2, file1 + # Wait for file deletion events (rm -rf deletes depth-first) unlink_events = [ Event(process=rm_process, event_type=EventType.UNLINK, file=file3, host_path=file3), @@ -267,8 +261,6 @@ def test_rmdir_ignored(monitored_dir, ignored_dir, server, fact_config): os.remove(monitored_file) os.rmdir(monitored_subdir) - # Only file deletion is reported as an event - # Directory deletions are tracked internally but not sent to sensors deletion_events = [ Event(process=process, event_type=EventType.UNLINK, file=monitored_file, host_path=monitored_file), @@ -335,7 +327,6 @@ def test_rmdir_with_parent_inode(monitored_dir, server, fact_config): os.rmdir(subdir) # Verify file deletion is tracked - # Directory deletions are tracked internally but not sent to sensors deletion_events = [ Event(process=process, event_type=EventType.UNLINK, file=test_file, host_path=test_file), From 51161be4a83ae348849a1d10b527edad2d50208f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 18 Apr 2026 12:00:25 -0700 Subject: [PATCH 11/15] Improved one logging statement --- fact-ebpf/src/bpf/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index b6ec78fe..53233363 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -338,7 +338,7 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { struct bound_path_t* path = path_read_append_d_entry(dir, dentry); if (path == NULL) { - bpf_printk("Failed to directory read path"); + bpf_printk("Failed to read directory path"); m->path_rmdir.error++; return 0; } From 89f44ef265b38a68b22754d4eff64c61d102444a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 19 Apr 2026 10:18:42 -0700 Subject: [PATCH 12/15] Removed unneeded comment --- fact-ebpf/src/bpf/main.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 53233363..2301bf7e 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -351,7 +351,6 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { return 0; } - // Remove directory inode from tracking inode_remove(&inode_key); submit_rmdir_event(&m->path_rmdir, From 815e068b1de0c8d26c22b222241be1eb2c470f42 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 23 Apr 2026 10:46:15 -0700 Subject: [PATCH 13/15] Renamed is_unlink to is_deletion --- fact/src/event/mod.rs | 2 +- fact/src/host_scanner.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 1b346548..163bf004 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -138,7 +138,7 @@ impl Event { matches!(self.file, FileData::RmDir(_)) } - pub fn is_unlink(&self) -> bool { + pub fn is_deletion(&self) -> bool { matches!(self.file, FileData::Unlink(_) | FileData::RmDir(_)) } diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 075cc870..d29ae1d6 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -291,7 +291,7 @@ impl HostScanner { } // Remove inode from the map - if event.is_unlink() { + if event.is_deletion() { self.handle_unlink_event(&event); } From 795136a5c9c62a8e2ef0e10ea584a19154b6ad9d Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 23 Apr 2026 11:25:02 -0700 Subject: [PATCH 14/15] Fix after rebase --- fact-ebpf/src/bpf/main.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 2301bf7e..dc3cd068 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -333,8 +333,9 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { if (m == NULL) { return 0; } + struct submit_event_args_t args = {.metrics = &m->path_rmdir}; - m->path_rmdir.total++; + args.metrics->total++; struct bound_path_t* path = path_read_append_d_entry(dir, dentry); if (path == NULL) { @@ -342,20 +343,17 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { m->path_rmdir.error++; return 0; } + args.filename = path->path; - inode_key_t inode_key = inode_to_key(dentry->d_inode); - inode_key_t* inode_to_submit = &inode_key; + args.inode = inode_to_key(dentry->d_inode); - if (is_monitored(inode_key, path, NULL, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(&args.inode, path, NULL) == NOT_MONITORED) { m->path_rmdir.ignored++; return 0; } - inode_remove(&inode_key); + inode_remove(&args.inode); - submit_rmdir_event(&m->path_rmdir, - path->path, - inode_to_submit, - NULL); + submit_rmdir_event(&args); return 0; } From 6e6e108b0f1b05e3599271f2c199020d0d5f3b88 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 23 Apr 2026 11:28:19 -0700 Subject: [PATCH 15/15] Added fact-ebpf/src/bpf/events.h which had been forgotten --- fact-ebpf/src/bpf/events.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 81307888..b10fb7a3 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -132,15 +132,11 @@ __always_inline static void submit_mkdir_event(struct submit_event_args_t* args) __submit_event(args, false); } -__always_inline static void submit_rmdir_event(struct metrics_by_hook_t* m, - const char filename[PATH_MAX], - inode_key_t* inode, - inode_key_t* parent_inode) { - struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); - if (event == NULL) { - m->ringbuffer_full++; +__always_inline static void submit_rmdir_event(struct submit_event_args_t* args) { + if (!reserve_event(args)) { return; } + args->event->type = DIR_ACTIVITY_UNLINK; - __submit_event(event, m, DIR_ACTIVITY_UNLINK, filename, inode, parent_inode, path_hooks_support_bpf_d_path); + __submit_event(args, path_hooks_support_bpf_d_path); }