From fd0fd1ce95c9e2a4301e0e96d6eb0c56013e1e52 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 10:13:00 -0700 Subject: [PATCH 01/32] X-Smart-Branch-Parent: main From 5b2167e0e48b8a813925db260f9540fce8c9f1e1 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 31 Mar 2026 16:41:02 -0700 Subject: [PATCH 02/32] Added integration tests --- tests/test_path_mkdir.py | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/test_path_mkdir.py diff --git a/tests/test_path_mkdir.py b/tests/test_path_mkdir.py new file mode 100644 index 00000000..5242894d --- /dev/null +++ b/tests/test_path_mkdir.py @@ -0,0 +1,69 @@ +import os + +import pytest + +from event import Event, EventType, Process + + +def test_mkdir_nested(monitored_dir, server): + """ + Tests that creating nested directories tracks all inodes correctly. + + Args: + monitored_dir: Temporary directory path for creating the test directory. + server: The server instance to communicate with. + """ + process = Process.from_proc() + + # Create nested directories + level1 = os.path.join(monitored_dir, 'level1') + level2 = os.path.join(level1, 'level2') + level3 = os.path.join(level2, 'level3') + + os.mkdir(level1) + os.mkdir(level2) + os.mkdir(level3) + + # Create a file in the deepest directory + test_file = os.path.join(level3, 'deep_file.txt') + with open(test_file, 'w') as f: + f.write('nested content') + + events = [ + Event(process=process, event_type=EventType.CREATION, + file=level1, host_path=level1), + Event(process=process, event_type=EventType.CREATION, + file=level2, host_path=level2), + Event(process=process, event_type=EventType.CREATION, + file=level3, host_path=level3), + Event(process=process, event_type=EventType.CREATION, + file=test_file, host_path=test_file), + ] + + server.wait_events(events) + + +def test_mkdir_ignored(monitored_dir, ignored_dir, server): + """ + Tests that directories created outside monitored paths are ignored. + + 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. + """ + process = Process.from_proc() + + # Create directory in ignored path - should not be tracked + ignored_subdir = os.path.join(ignored_dir, 'ignored_subdir') + os.mkdir(ignored_subdir) + + # Create directory in monitored path - should be tracked + monitored_subdir = os.path.join(monitored_dir, 'monitored_subdir') + os.mkdir(monitored_subdir) + + # Only the monitored directory should generate an event + e = Event(process=process, event_type=EventType.CREATION, + file=monitored_subdir, host_path=monitored_subdir) + + server.wait_events([e]) From 12f602eb08fd8f6f0b70a40f6ad4b34507eab4cc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 1 Apr 2026 10:50:56 -0700 Subject: [PATCH 03/32] Instrument inode tracking on directory being created --- fact-ebpf/src/bpf/events.h | 14 ++++ fact-ebpf/src/bpf/file.h | 16 +++++ fact-ebpf/src/bpf/main.c | 107 +++++++++++++++++++++++++++++ fact-ebpf/src/bpf/maps.h | 20 ++++++ fact-ebpf/src/bpf/types.h | 8 +++ fact/src/metrics/kernel_metrics.rs | 18 +++++ 6 files changed, 183 insertions(+) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 26254778..5fc3d3a6 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -129,3 +129,17 @@ __always_inline static void submit_rename_event(struct metrics_by_hook_t* m, __submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, new_parent_inode, path_hooks_support_bpf_d_path); } + +__always_inline static void submit_mkdir_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; + } + + // d_instantiate doesn't support bpf_d_path, so we use false and rely on the stashed path from path_mkdir + __submit_event(event, m, FILE_ACTIVITY_CREATION, filename, inode, parent_inode, false); +} diff --git a/fact-ebpf/src/bpf/file.h b/fact-ebpf/src/bpf/file.h index d0fdc8b1..eac1b429 100644 --- a/fact-ebpf/src/bpf/file.h +++ b/fact-ebpf/src/bpf/file.h @@ -43,3 +43,19 @@ __always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct return NOT_MONITORED; } + +// Check if a new directory should be tracked based on its parent and path. +// This is used during mkdir operations where the child inode doesn't exist yet. +__always_inline static inode_monitored_t should_track_mkdir(inode_key_t parent_inode, struct bound_path_t* child_path) { + const inode_value_t* volatile parent_value = inode_get(&parent_inode); + + if (parent_value != NULL) { + return PARENT_MONITORED; + } + + if (path_is_monitored(child_path)) { + return MONITORED; + } + + return NOT_MONITORED; +} diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 5ac35159..bef5df6c 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -19,6 +19,11 @@ char _license[] SEC("license") = "Dual MIT/GPL"; #define FMODE_PWRITE ((fmode_t)(1 << 4)) #define FMODE_CREATED ((fmode_t)(1 << 20)) +// File type constants from linux/stat.h +#define S_IFMT 00170000 +#define S_IFDIR 0040000 +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) + SEC("lsm/file_open") int BPF_PROG(trace_file_open, struct file* file) { struct metrics_t* m = get_metrics(); @@ -231,3 +236,105 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, m->path_rename.error++; return 0; } + +SEC("lsm/path_mkdir") +int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t mode) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->path_mkdir.total++; + + struct bound_path_t* path = path_read_append_d_entry(dir, dentry); + if (path == NULL) { + bpf_printk("Failed to read path"); + m->path_mkdir.error++; + return 0; + } + + struct dentry* parent_dentry = BPF_CORE_READ(dir, dentry); + struct inode* parent_inode_ptr = BPF_CORE_READ(parent_dentry, d_inode); + inode_key_t parent_inode = inode_to_key(parent_inode_ptr); + + if (should_track_mkdir(parent_inode, path) == NOT_MONITORED) { + m->path_mkdir.ignored++; + return 0; + } + + // Stash mkdir context for security_d_instantiate + __u64 pid_tgid = bpf_get_current_pid_tgid(); + struct mkdir_context_t* mkdir_ctx = get_mkdir_context(); + if (mkdir_ctx == NULL) { + bpf_printk("Failed to get mkdir context buffer"); + m->path_mkdir.error++; + return 0; + } + + long path_copy_len = bpf_probe_read_str(mkdir_ctx->path, PATH_MAX, path->path); + if (path_copy_len < 0) { + bpf_printk("Failed to copy path string"); + m->path_mkdir.error++; + return 0; + } + mkdir_ctx->parent_inode = parent_inode; + + if (bpf_map_update_elem(&mkdir_context, &pid_tgid, mkdir_ctx, BPF_ANY) != 0) { + bpf_printk("Failed to stash mkdir context"); + m->path_mkdir.error++; + return 0; + } + + return 0; +} + +SEC("lsm/d_instantiate") +int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->d_instantiate.total++; + + if (inode == NULL) { + m->d_instantiate.ignored++; + return 0; + } + + __u64 pid_tgid = bpf_get_current_pid_tgid(); + struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); + if (mkdir_ctx == NULL) { + m->d_instantiate.ignored++; + return 0; + } + + // Check if this is a directory + umode_t mode = BPF_CORE_READ(inode, i_mode); + if (!S_ISDIR(mode)) { + bpf_map_delete_elem(&mkdir_context, &pid_tgid); + m->d_instantiate.ignored++; + return 0; + } + + // Get the inode key for the new directory + inode_key_t inode_key = inode_to_key(inode); + + // Add the new directory inode to tracking + if (inode_add(&inode_key) == 0) { + m->d_instantiate.added++; + } else { + m->d_instantiate.error++; + } + + // Submit creation event using the stashed path + submit_mkdir_event(&m->d_instantiate, + mkdir_ctx->path, + &inode_key, + &mkdir_ctx->parent_inode); + + // Clean up context + bpf_map_delete_elem(&mkdir_context, &pid_tgid); + + return 0; +} diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index eca822f0..3758ea80 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -83,6 +83,26 @@ struct { __uint(map_flags, BPF_F_NO_PREALLOC); } inode_map SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __type(key, __u32); + __type(value, struct mkdir_context_t); + __uint(max_entries, 1); +} mkdir_context_heap SEC(".maps"); + +__always_inline static struct mkdir_context_t* get_mkdir_context() { + unsigned int zero = 0; + return bpf_map_lookup_elem(&mkdir_context_heap, &zero); +} + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u64); + __type(value, struct mkdir_context_t); + __uint(max_entries, 16384); + __uint(map_flags, BPF_F_NO_PREALLOC); +} mkdir_context SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __type(key, __u32); diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 55005c00..9059c7aa 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -96,6 +96,12 @@ struct path_prefix_t { const char path[LPM_SIZE_MAX]; }; +// Context for correlating mkdir operations +struct mkdir_context_t { + char path[PATH_MAX]; + inode_key_t parent_inode; +}; + // Metrics types struct metrics_by_hook_t { unsigned long long total; @@ -111,4 +117,6 @@ struct metrics_t { struct metrics_by_hook_t path_chmod; struct metrics_by_hook_t path_chown; struct metrics_by_hook_t path_rename; + struct metrics_by_hook_t path_mkdir; + struct metrics_by_hook_t d_instantiate; }; diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index d1a3a242..9caa1ff3 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -13,6 +13,8 @@ pub struct KernelMetrics { path_chmod: EventCounter, path_chown: EventCounter, path_rename: EventCounter, + path_mkdir: EventCounter, + d_instantiate: EventCounter, map: PerCpuArray, } @@ -43,12 +45,24 @@ impl KernelMetrics { "Events processed by the path_rename LSM hook", &[], // Labels are not needed since `collect` will add them all ); + let path_mkdir = EventCounter::new( + "kernel_path_mkdir_events", + "Events processed by the path_mkdir 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", + &[], // Labels are not needed since `collect` will add them all + ); file_open.register(reg); path_unlink.register(reg); path_chmod.register(reg); path_chown.register(reg); path_rename.register(reg); + path_mkdir.register(reg); + d_instantiate.register(reg); KernelMetrics { file_open, @@ -56,6 +70,8 @@ impl KernelMetrics { path_chmod, path_chown, path_rename, + path_mkdir, + d_instantiate, map: kernel_metrics, } } @@ -105,6 +121,8 @@ impl KernelMetrics { KernelMetrics::refresh_labels(&self.path_chmod, &metrics.path_chmod); 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.d_instantiate, &metrics.d_instantiate); Ok(()) } From 8406a5afd157195551b005a293c180ae9b00a87f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 1 Apr 2026 15:29:42 -0700 Subject: [PATCH 04/32] Only tracking directories if the parent is monitored --- 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 bef5df6c..1ec2837d 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -257,7 +257,7 @@ int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t struct inode* parent_inode_ptr = BPF_CORE_READ(parent_dentry, d_inode); inode_key_t parent_inode = inode_to_key(parent_inode_ptr); - if (should_track_mkdir(parent_inode, path) == NOT_MONITORED) { + if (should_track_mkdir(parent_inode, path) != PARENT_MONITORED) { m->path_mkdir.ignored++; return 0; } From 03bd26ebcff3edb940d4740a2ecd0974adf1616f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 1 Apr 2026 16:41:52 -0700 Subject: [PATCH 05/32] Removed some comments --- fact-ebpf/src/bpf/main.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 1ec2837d..ea0781ea 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -309,7 +309,6 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { return 0; } - // Check if this is a directory umode_t mode = BPF_CORE_READ(inode, i_mode); if (!S_ISDIR(mode)) { bpf_map_delete_elem(&mkdir_context, &pid_tgid); @@ -317,23 +316,19 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { return 0; } - // Get the inode key for the new directory inode_key_t inode_key = inode_to_key(inode); - // Add the new directory inode to tracking if (inode_add(&inode_key) == 0) { m->d_instantiate.added++; } else { m->d_instantiate.error++; } - // Submit creation event using the stashed path submit_mkdir_event(&m->d_instantiate, mkdir_ctx->path, &inode_key, &mkdir_ctx->parent_inode); - // Clean up context bpf_map_delete_elem(&mkdir_context, &pid_tgid); return 0; From b1fc6f57fe562a742d72a39fcde73a6e0a506ff4 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 2 Apr 2026 10:43:48 -0700 Subject: [PATCH 06/32] Combined two uses of BPF_CORE_READ --- fact-ebpf/src/bpf/main.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index ea0781ea..743ad73c 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -253,8 +253,7 @@ int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t return 0; } - struct dentry* parent_dentry = BPF_CORE_READ(dir, dentry); - struct inode* parent_inode_ptr = BPF_CORE_READ(parent_dentry, d_inode); + struct inode* parent_inode_ptr = BPF_CORE_READ(dir, dentry, d_inode); inode_key_t parent_inode = inode_to_key(parent_inode_ptr); if (should_track_mkdir(parent_inode, path) != PARENT_MONITORED) { From dbea1a72e9f07a964c2c41970d0721af7f71d2ab Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 2 Apr 2026 11:25:58 -0700 Subject: [PATCH 07/32] Added DIR_ACTIVITY_CREATION --- fact-ebpf/src/bpf/events.h | 2 +- fact-ebpf/src/bpf/types.h | 1 + fact/src/event/mod.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 5fc3d3a6..fe521450 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -141,5 +141,5 @@ __always_inline static void submit_mkdir_event(struct metrics_by_hook_t* m, } // d_instantiate doesn't support bpf_d_path, so we use false and rely on the stashed path from path_mkdir - __submit_event(event, m, FILE_ACTIVITY_CREATION, filename, inode, parent_inode, false); + __submit_event(event, m, DIR_ACTIVITY_CREATION, filename, inode, parent_inode, false); } diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 9059c7aa..95c67e86 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -55,6 +55,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_CHMOD, FILE_ACTIVITY_CHOWN, FILE_ACTIVITY_RENAME, + DIR_ACTIVITY_CREATION, } file_activity_type_t; struct event_t { diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index bc9d7897..ed83b899 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -311,6 +311,7 @@ impl FileData { let file = match event_type { 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::Creation(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_CHMOD => { let data = ChmodFileData { From d1b4136ebfbb5570d9b8a3aa058d5025c04def4a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 2 Apr 2026 11:29:22 -0700 Subject: [PATCH 08/32] Added permalink to linux/stat.h --- fact-ebpf/src/bpf/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 743ad73c..e40cf1c1 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -20,6 +20,7 @@ char _license[] SEC("license") = "Dual MIT/GPL"; #define FMODE_CREATED ((fmode_t)(1 << 20)) // File type constants from linux/stat.h +// https://github.com/torvalds/linux/blob/5619b098e2fbf3a23bf13d91897056a1fe238c6d/include/uapi/linux/stat.h #define S_IFMT 00170000 #define S_IFDIR 0040000 #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) From a1200841270e02127165b39039bf96a106ca0d23 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 2 Apr 2026 11:43:15 -0700 Subject: [PATCH 09/32] Removing map entry in case of early return in lsm/d_instantiate --- fact-ebpf/src/bpf/main.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index e40cf1c1..a7841284 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -309,11 +309,11 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { return 0; } + // From this point on, we must clean up mkdir_context before returning umode_t mode = BPF_CORE_READ(inode, i_mode); if (!S_ISDIR(mode)) { - bpf_map_delete_elem(&mkdir_context, &pid_tgid); m->d_instantiate.ignored++; - return 0; + goto cleanup; } inode_key_t inode_key = inode_to_key(inode); @@ -329,7 +329,7 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { &inode_key, &mkdir_ctx->parent_inode); +cleanup: bpf_map_delete_elem(&mkdir_context, &pid_tgid); - return 0; } From fb7ce804b200a288040f34a153464d076b3ac5a3 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 2 Apr 2026 15:25:20 -0700 Subject: [PATCH 10/32] Using os.makedirs instead of os.mkdir three times --- tests/test_path_mkdir.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_path_mkdir.py b/tests/test_path_mkdir.py index 5242894d..759e0022 100644 --- a/tests/test_path_mkdir.py +++ b/tests/test_path_mkdir.py @@ -20,9 +20,7 @@ def test_mkdir_nested(monitored_dir, server): level2 = os.path.join(level1, 'level2') level3 = os.path.join(level2, 'level3') - os.mkdir(level1) - os.mkdir(level2) - os.mkdir(level3) + os.makedirs(level3, exist_ok=True) # Create a file in the deepest directory test_file = os.path.join(level3, 'deep_file.txt') From 60f96eb6f7a9f7979490dddc59b0005d76962688 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 3 Apr 2026 10:55:54 -0700 Subject: [PATCH 11/32] Accumulate m.path_mkdir and m.d_instantiate --- fact-ebpf/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index bd84ee08..c4c95ec6 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -125,6 +125,8 @@ impl metrics_t { m.path_chmod = m.path_chmod.accumulate(&other.path_chmod); 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.d_instantiate = m.d_instantiate.accumulate(&other.d_instantiate); m } } From 651edd47ceb2a4cd41972fb1483522a3dd562519 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 3 Apr 2026 11:58:44 -0700 Subject: [PATCH 12/32] Checking pid_tgid earlier so if inode is null we still cleanup --- fact-ebpf/src/bpf/main.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index a7841284..2fead541 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -297,19 +297,19 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { m->d_instantiate.total++; - if (inode == NULL) { - m->d_instantiate.ignored++; - return 0; - } - __u64 pid_tgid = bpf_get_current_pid_tgid(); struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); + if (mkdir_ctx == NULL) { m->d_instantiate.ignored++; return 0; } - // From this point on, we must clean up mkdir_context before returning + if (inode == NULL) { + m->d_instantiate.ignored++; + goto cleanup; + } + umode_t mode = BPF_CORE_READ(inode, i_mode); if (!S_ISDIR(mode)) { m->d_instantiate.ignored++; From a57baf7e2aa6face7da768b9e24cd97e88fb3e78 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 6 Apr 2026 17:09:10 -0700 Subject: [PATCH 13/32] Not using BPF_F_NO_PREALLOC --- fact-ebpf/src/bpf/main.c | 18 ++++++++++-------- fact-ebpf/src/bpf/maps.h | 13 ------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 2fead541..a6521cfd 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -264,9 +264,16 @@ int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t // Stash mkdir context for security_d_instantiate __u64 pid_tgid = bpf_get_current_pid_tgid(); - struct mkdir_context_t* mkdir_ctx = get_mkdir_context(); + + if (bpf_map_update_elem(&mkdir_context, &pid_tgid, NULL, BPF_ANY) != 0) { + bpf_printk("Failed to create mkdir context entry"); + m->path_mkdir.error++; + return 0; + } + + struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); if (mkdir_ctx == NULL) { - bpf_printk("Failed to get mkdir context buffer"); + bpf_printk("Failed to lookup mkdir context after creation"); m->path_mkdir.error++; return 0; } @@ -275,16 +282,11 @@ int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t if (path_copy_len < 0) { bpf_printk("Failed to copy path string"); m->path_mkdir.error++; + bpf_map_delete_elem(&mkdir_context, &pid_tgid); return 0; } mkdir_ctx->parent_inode = parent_inode; - if (bpf_map_update_elem(&mkdir_context, &pid_tgid, mkdir_ctx, BPF_ANY) != 0) { - bpf_printk("Failed to stash mkdir context"); - m->path_mkdir.error++; - return 0; - } - return 0; } diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index 3758ea80..acd498f7 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -83,24 +83,11 @@ struct { __uint(map_flags, BPF_F_NO_PREALLOC); } inode_map SEC(".maps"); -struct { - __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); - __type(key, __u32); - __type(value, struct mkdir_context_t); - __uint(max_entries, 1); -} mkdir_context_heap SEC(".maps"); - -__always_inline static struct mkdir_context_t* get_mkdir_context() { - unsigned int zero = 0; - return bpf_map_lookup_elem(&mkdir_context_heap, &zero); -} - struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u64); __type(value, struct mkdir_context_t); __uint(max_entries, 16384); - __uint(map_flags, BPF_F_NO_PREALLOC); } mkdir_context SEC(".maps"); struct { From 1fa3c77d91b39693a1df835342e5b438d09b2498 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 6 Apr 2026 18:46:35 -0700 Subject: [PATCH 14/32] Switched from BPF_MAP_TYPE_HASH to BPF_MAP_TYPE_LRU_HASH --- fact-ebpf/src/bpf/maps.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index acd498f7..c8595829 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -84,7 +84,7 @@ struct { } inode_map SEC(".maps"); struct { - __uint(type, BPF_MAP_TYPE_HASH); + __uint(type, BPF_MAP_TYPE_LRU_HASH); __type(key, __u64); __type(value, struct mkdir_context_t); __uint(max_entries, 16384); From 0d1929d667e9b79d6821c4b208c8ae0750f260f9 Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Mon, 6 Apr 2026 18:53:01 -0700 Subject: [PATCH 15/32] Apply suggestion from @Molter73 Co-authored-by: Mauro Ezequiel Moltrasio --- fact/src/event/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index ed83b899..70e96916 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -310,7 +310,7 @@ impl FileData { let inner = BaseFileData::new(filename, inode, parent_inode)?; let file = match event_type { file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), - file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner), + file_activity_type_t::FILE_ACTIVITY_CREATION | file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::Creation(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_CHMOD => { From e9634e57bb48f5f4a31c9240b20e8d3c2c1050d5 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 6 Apr 2026 19:01:45 -0700 Subject: [PATCH 16/32] Parameterized test --- tests/test_path_mkdir.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_path_mkdir.py b/tests/test_path_mkdir.py index 759e0022..a3953e50 100644 --- a/tests/test_path_mkdir.py +++ b/tests/test_path_mkdir.py @@ -5,20 +5,27 @@ from event import Event, EventType, Process -def test_mkdir_nested(monitored_dir, server): +@pytest.mark.parametrize("dirname", [ + pytest.param('level3', id='ASCII'), + pytest.param('café', id='French'), + pytest.param('файл', id='Cyrillic'), + pytest.param('日本語', id='Japanese'), +]) +def test_mkdir_nested(monitored_dir, server, dirname): """ Tests that creating nested directories tracks all inodes correctly. Args: monitored_dir: Temporary directory path for creating the test directory. server: The server instance to communicate with. + dirname: Final directory name to test (including UTF-8 variants). """ process = Process.from_proc() # Create nested directories level1 = os.path.join(monitored_dir, 'level1') level2 = os.path.join(level1, 'level2') - level3 = os.path.join(level2, 'level3') + level3 = os.path.join(level2, dirname) os.makedirs(level3, exist_ok=True) From 11eba2e14c57b97e922e0dbb47f7216e9b8e292f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 6 Apr 2026 22:13:37 -0700 Subject: [PATCH 17/32] Fixed verifier issue. Not sending directory creation events --- fact-ebpf/src/bpf/main.c | 22 ++++++++++++---------- fact/src/event/mod.rs | 8 ++++++++ fact/src/host_scanner.rs | 9 +++++++++ tests/test_path_mkdir.py | 18 ++++++++++-------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index a6521cfd..f480418c 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -264,18 +264,20 @@ int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t // Stash mkdir context for security_d_instantiate __u64 pid_tgid = bpf_get_current_pid_tgid(); - - if (bpf_map_update_elem(&mkdir_context, &pid_tgid, NULL, BPF_ANY) != 0) { - bpf_printk("Failed to create mkdir context entry"); - m->path_mkdir.error++; - return 0; - } - struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); if (mkdir_ctx == NULL) { - bpf_printk("Failed to lookup mkdir context after creation"); - m->path_mkdir.error++; - return 0; + static const struct mkdir_context_t empty_ctx = {0}; + if (bpf_map_update_elem(&mkdir_context, &pid_tgid, &empty_ctx, BPF_NOEXIST) != 0) { + bpf_printk("Failed to create mkdir context entry"); + m->path_mkdir.error++; + return 0; + } + mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); + if (mkdir_ctx == NULL) { + bpf_printk("Failed to lookup mkdir context after creation"); + m->path_mkdir.error++; + return 0; + } } long path_copy_len = bpf_probe_read_str(mkdir_ctx->path, PATH_MAX, path->path); diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 70e96916..2d801c54 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -74,6 +74,8 @@ pub struct Event { hostname: &'static str, process: Process, file: FileData, + #[serde(skip)] + event_type: file_activity_type_t, } impl Event { @@ -123,6 +125,7 @@ impl Event { hostname, process, file, + event_type: file_activity_type_t::FILE_ACTIVITY_CREATION, }) } @@ -133,6 +136,10 @@ impl Event { pub fn is_unlink(&self) -> bool { matches!(self.file, FileData::Unlink(_)) } + + pub fn is_dir_creation(&self) -> bool { + self.event_type == file_activity_type_t::DIR_ACTIVITY_CREATION + } /// Unwrap the inner FileData and return the inode that triggered /// the event. @@ -263,6 +270,7 @@ impl TryFrom<&event_t> for Event { hostname: host_info::get_hostname(), process, file, + event_type: value.type_, }) } } diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 19d5b719..b755332e 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -306,6 +306,15 @@ impl HostScanner { self.metrics.events.dropped(); warn!("Failed to send event: {e}"); } + + // Skip directory creation events - we track them internally but don't send to sensor + if !event.is_dir_creation() { + let event = Arc::new(event); + if let Err(e) = self.tx.send(event) { + self.metrics.events.dropped(); + warn!("Failed to send event: {e}"); + } + } }, _ = scan_trigger.notified() => self.scan()?, _ = self.paths.changed() => self.scan()?, diff --git a/tests/test_path_mkdir.py b/tests/test_path_mkdir.py index a3953e50..89fd3b72 100644 --- a/tests/test_path_mkdir.py +++ b/tests/test_path_mkdir.py @@ -34,13 +34,9 @@ def test_mkdir_nested(monitored_dir, server, dirname): with open(test_file, 'w') as f: f.write('nested content') + # Directory creation events are tracked internally but not sent to sensor + # Only the file creation event should be sent events = [ - Event(process=process, event_type=EventType.CREATION, - file=level1, host_path=level1), - Event(process=process, event_type=EventType.CREATION, - file=level2, host_path=level2), - Event(process=process, event_type=EventType.CREATION, - file=level3, host_path=level3), Event(process=process, event_type=EventType.CREATION, file=test_file, host_path=test_file), ] @@ -62,13 +58,19 @@ def test_mkdir_ignored(monitored_dir, ignored_dir, server): # Create directory in ignored path - should not be tracked 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') # Create directory in monitored path - should be tracked 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') - # Only the monitored directory should generate an event + # Only the monitored file should generate an event (directories are tracked internally) e = Event(process=process, event_type=EventType.CREATION, - file=monitored_subdir, host_path=monitored_subdir) + file=monitored_file, host_path=monitored_file) server.wait_events([e]) From 008b580b889be7758307906682ee4ae763ce3bf6 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 7 Apr 2026 07:40:47 -0700 Subject: [PATCH 18/32] make format --- fact/src/event/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 2d801c54..a69d4dce 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -318,8 +318,8 @@ impl FileData { let inner = BaseFileData::new(filename, inode, parent_inode)?; let file = match event_type { file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), - file_activity_type_t::FILE_ACTIVITY_CREATION | - file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::Creation(inner), + file_activity_type_t::FILE_ACTIVITY_CREATION + | file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::Creation(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_CHMOD => { let data = ChmodFileData { From e86fa4eef6a74a8ae271c2df800e6caf6cace7cc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 8 Apr 2026 08:57:44 -0700 Subject: [PATCH 19/32] Improved test Event constructor --- fact/src/event/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index a69d4dce..fad42d42 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -97,16 +97,16 @@ impl Event { inode: Default::default(), parent_inode: Default::default(), }; - let file = match data { - EventTestData::Creation => FileData::Creation(inner), - EventTestData::Unlink => FileData::Unlink(inner), + let (file, event_type) = match data { + EventTestData::Creation => (FileData::Creation(inner), file_activity_type_t::FILE_ACTIVITY_CREATION), + EventTestData::Unlink => (FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK), EventTestData::Chmod(new_mode, old_mode) => { let data = ChmodFileData { inner, new_mode, old_mode, }; - FileData::Chmod(data) + (FileData::Chmod(data), file_activity_type_t::FILE_ACTIVITY_CHMOD) } EventTestData::Rename(old_path) => { let data = RenameFileData { @@ -116,7 +116,7 @@ impl Event { ..Default::default() }, }; - FileData::Rename(data) + (FileData::Rename(data), file_activity_type_t::FILE_ACTIVITY_RENAME) } }; @@ -125,7 +125,7 @@ impl Event { hostname, process, file, - event_type: file_activity_type_t::FILE_ACTIVITY_CREATION, + event_type, }) } From fd6bcbb4a3ccd61a28faab7371f4ad98ff92ed38 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 8 Apr 2026 09:17:06 -0700 Subject: [PATCH 20/32] Remove is_dir_creation --- fact/src/host_scanner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index b755332e..b544cecb 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -308,7 +308,7 @@ impl HostScanner { } // Skip directory creation events - we track them internally but don't send to sensor - if !event.is_dir_creation() { + if event.event_type() != fact_ebpf::file_activity_type_t::DIR_ACTIVITY_CREATION { let event = Arc::new(event); if let Err(e) = self.tx.send(event) { self.metrics.events.dropped(); From 5f83efa1d701e5bd7d0528d85976e9a97dc28616 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 8 Apr 2026 09:45:02 -0700 Subject: [PATCH 21/32] Removed unneeded S_ISDIR --- fact-ebpf/src/bpf/main.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index f480418c..8a601e15 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -19,12 +19,6 @@ char _license[] SEC("license") = "Dual MIT/GPL"; #define FMODE_PWRITE ((fmode_t)(1 << 4)) #define FMODE_CREATED ((fmode_t)(1 << 20)) -// File type constants from linux/stat.h -// https://github.com/torvalds/linux/blob/5619b098e2fbf3a23bf13d91897056a1fe238c6d/include/uapi/linux/stat.h -#define S_IFMT 00170000 -#define S_IFDIR 0040000 -#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) - SEC("lsm/file_open") int BPF_PROG(trace_file_open, struct file* file) { struct metrics_t* m = get_metrics(); @@ -314,12 +308,6 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { goto cleanup; } - umode_t mode = BPF_CORE_READ(inode, i_mode); - if (!S_ISDIR(mode)) { - m->d_instantiate.ignored++; - goto cleanup; - } - inode_key_t inode_key = inode_to_key(inode); if (inode_add(&inode_key) == 0) { From b97f03ec0147eda1762b2c69bbaeff2c5e4b192f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 8 Apr 2026 09:51:21 -0700 Subject: [PATCH 22/32] make format --- fact/src/event/mod.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index fad42d42..41f2cb67 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -98,15 +98,24 @@ impl Event { parent_inode: Default::default(), }; let (file, event_type) = match data { - EventTestData::Creation => (FileData::Creation(inner), file_activity_type_t::FILE_ACTIVITY_CREATION), - EventTestData::Unlink => (FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK), + EventTestData::Creation => ( + FileData::Creation(inner), + file_activity_type_t::FILE_ACTIVITY_CREATION, + ), + EventTestData::Unlink => ( + FileData::Unlink(inner), + file_activity_type_t::FILE_ACTIVITY_UNLINK, + ), EventTestData::Chmod(new_mode, old_mode) => { let data = ChmodFileData { inner, new_mode, old_mode, }; - (FileData::Chmod(data), file_activity_type_t::FILE_ACTIVITY_CHMOD) + ( + FileData::Chmod(data), + file_activity_type_t::FILE_ACTIVITY_CHMOD, + ) } EventTestData::Rename(old_path) => { let data = RenameFileData { @@ -116,7 +125,10 @@ impl Event { ..Default::default() }, }; - (FileData::Rename(data), file_activity_type_t::FILE_ACTIVITY_RENAME) + ( + FileData::Rename(data), + file_activity_type_t::FILE_ACTIVITY_RENAME, + ) } }; From 0874869bad414a06cb3cc4ed46e7e0c26079e899 Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Thu, 9 Apr 2026 10:11:48 -0700 Subject: [PATCH 23/32] Update tests/test_path_mkdir.py Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_mkdir.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_path_mkdir.py b/tests/test_path_mkdir.py index 89fd3b72..88685f8f 100644 --- a/tests/test_path_mkdir.py +++ b/tests/test_path_mkdir.py @@ -23,14 +23,11 @@ def test_mkdir_nested(monitored_dir, server, dirname): process = Process.from_proc() # Create nested directories - level1 = os.path.join(monitored_dir, 'level1') - level2 = os.path.join(level1, 'level2') - level3 = os.path.join(level2, dirname) - - os.makedirs(level3, exist_ok=True) + test_dir = os.path.join(monitored_dir, 'level1', 'level2', dirname) + os.makedirs(test_dir, exist_ok=True) # Create a file in the deepest directory - test_file = os.path.join(level3, 'deep_file.txt') + test_file = os.path.join(test_dir, 'deep_file.txt') with open(test_file, 'w') as f: f.write('nested content') From ba14b12255bf7a361bb3cf8acb3c74b7f18d4831 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 10:51:35 -0700 Subject: [PATCH 24/32] Fixes after rebase --- fact/src/event/mod.rs | 4 ++++ fact/src/host_scanner.rs | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 41f2cb67..3bb10fb0 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -145,6 +145,10 @@ impl Event { matches!(self.file, FileData::Creation(_)) } + pub(crate) fn event_type(&self) -> file_activity_type_t { + self.event_type + } + pub fn is_unlink(&self) -> bool { matches!(self.file, FileData::Unlink(_)) } diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index b544cecb..010d2331 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -301,12 +301,6 @@ impl HostScanner { self.handle_unlink_event(&event); } - let event = Arc::new(event); - if let Err(e) = self.tx.send(event) { - self.metrics.events.dropped(); - warn!("Failed to send event: {e}"); - } - // Skip directory creation events - we track them internally but don't send to sensor if event.event_type() != fact_ebpf::file_activity_type_t::DIR_ACTIVITY_CREATION { let event = Arc::new(event); From e4cd88235b9c8f61dbf4980df0af4e83cb16ef6b Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 10:56:48 -0700 Subject: [PATCH 25/32] Moved the check for null inode up --- fact-ebpf/src/bpf/main.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 8a601e15..d262f2d8 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -296,16 +296,17 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { m->d_instantiate.total++; __u64 pid_tgid = bpf_get_current_pid_tgid(); - struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); - if (mkdir_ctx == NULL) { + if (inode == NULL) { m->d_instantiate.ignored++; - return 0; + goto cleanup; } - if (inode == NULL) { + struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid); + + if (mkdir_ctx == NULL) { m->d_instantiate.ignored++; - goto cleanup; + return 0; } inode_key_t inode_key = inode_to_key(inode); From 34f8ff41f26876702730687013545d030bd99498 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 13:42:41 -0700 Subject: [PATCH 26/32] Not adding a new event type field to Event struct --- fact/src/event/mod.rs | 56 +++++++++++++++++++--------------------- fact/src/host_scanner.rs | 4 +-- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 3bb10fb0..9f87430b 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -7,6 +7,7 @@ use std::{ }; use globset::GlobSet; +use log::warn; use serde::Serialize; use fact_ebpf::{PATH_MAX, event_t, file_activity_type_t, inode_key_t}; @@ -74,8 +75,6 @@ pub struct Event { hostname: &'static str, process: Process, file: FileData, - #[serde(skip)] - event_type: file_activity_type_t, } impl Event { @@ -97,25 +96,16 @@ impl Event { inode: Default::default(), parent_inode: Default::default(), }; - let (file, event_type) = match data { - EventTestData::Creation => ( - FileData::Creation(inner), - file_activity_type_t::FILE_ACTIVITY_CREATION, - ), - EventTestData::Unlink => ( - FileData::Unlink(inner), - file_activity_type_t::FILE_ACTIVITY_UNLINK, - ), + let file = match data { + EventTestData::Creation => FileData::Creation(inner), + EventTestData::Unlink => FileData::Unlink(inner), EventTestData::Chmod(new_mode, old_mode) => { let data = ChmodFileData { inner, new_mode, old_mode, }; - ( - FileData::Chmod(data), - file_activity_type_t::FILE_ACTIVITY_CHMOD, - ) + FileData::Chmod(data) } EventTestData::Rename(old_path) => { let data = RenameFileData { @@ -125,10 +115,7 @@ impl Event { ..Default::default() }, }; - ( - FileData::Rename(data), - file_activity_type_t::FILE_ACTIVITY_RENAME, - ) + FileData::Rename(data) } }; @@ -137,25 +124,24 @@ impl Event { hostname, process, file, - event_type, }) } pub fn is_creation(&self) -> bool { + matches!(self.file, FileData::Creation(_) | FileData::MkDir(_)) + } + + pub fn is_file_creation(&self) -> bool { matches!(self.file, FileData::Creation(_)) } - pub(crate) fn event_type(&self) -> file_activity_type_t { - self.event_type + pub fn is_mkdir(&self) -> bool { + matches!(self.file, FileData::MkDir(_)) } pub fn is_unlink(&self) -> bool { matches!(self.file, FileData::Unlink(_)) } - - pub fn is_dir_creation(&self) -> bool { - self.event_type == file_activity_type_t::DIR_ACTIVITY_CREATION - } /// Unwrap the inner FileData and return the inode that triggered /// the event. @@ -166,6 +152,7 @@ impl Event { match &self.file { FileData::Open(data) => &data.inode, FileData::Creation(data) => &data.inode, + FileData::MkDir(data) => &data.inode, FileData::Unlink(data) => &data.inode, FileData::Chmod(data) => &data.inner.inode, FileData::Chown(data) => &data.inner.inode, @@ -178,6 +165,7 @@ impl Event { match &self.file { FileData::Open(data) => &data.parent_inode, FileData::Creation(data) => &data.parent_inode, + FileData::MkDir(data) => &data.parent_inode, FileData::Unlink(data) => &data.parent_inode, FileData::Chmod(data) => &data.inner.parent_inode, FileData::Chown(data) => &data.inner.parent_inode, @@ -199,6 +187,7 @@ impl Event { match &self.file { FileData::Open(data) => &data.filename, FileData::Creation(data) => &data.filename, + FileData::MkDir(data) => &data.filename, FileData::Unlink(data) => &data.filename, FileData::Chmod(data) => &data.inner.filename, FileData::Chown(data) => &data.inner.filename, @@ -217,6 +206,7 @@ impl Event { match &self.file { FileData::Open(data) => &data.host_file, FileData::Creation(data) => &data.host_file, + FileData::MkDir(data) => &data.host_file, FileData::Unlink(data) => &data.host_file, FileData::Chmod(data) => &data.inner.host_file, FileData::Chown(data) => &data.inner.host_file, @@ -232,6 +222,7 @@ impl Event { match &mut self.file { 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::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, @@ -286,7 +277,6 @@ impl TryFrom<&event_t> for Event { hostname: host_info::get_hostname(), process, file, - event_type: value.type_, }) } } @@ -317,6 +307,7 @@ impl PartialEq for Event { pub enum FileData { Open(BaseFileData), Creation(BaseFileData), + MkDir(BaseFileData), Unlink(BaseFileData), Chmod(ChmodFileData), Chown(ChownFileData), @@ -334,8 +325,8 @@ impl FileData { let inner = BaseFileData::new(filename, inode, parent_inode)?; let file = match event_type { file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), - file_activity_type_t::FILE_ACTIVITY_CREATION - | file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::Creation(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::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), file_activity_type_t::FILE_ACTIVITY_CHMOD => { let data = ChmodFileData { @@ -384,6 +375,12 @@ impl From for fact_api::file_activity::File { let f_act = fact_api::FileCreation { activity }; fact_api::file_activity::File::Creation(f_act) } + FileData::MkDir(event) => { + warn!("MkDir event reached protobuf conversion - converting to Creation (filtering may have failed)"); + let activity = Some(fact_api::FileActivityBase::from(event)); + let f_act = fact_api::FileCreation { activity }; + fact_api::file_activity::File::Creation(f_act) + } FileData::Unlink(event) => { let activity = Some(fact_api::FileActivityBase::from(event)); let f_act = fact_api::FileUnlink { activity }; @@ -411,6 +408,7 @@ impl PartialEq for FileData { match (self, other) { (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::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/host_scanner.rs b/fact/src/host_scanner.rs index 010d2331..f49c621f 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -280,7 +280,7 @@ impl HostScanner { }; self.metrics.events.added(); - // Handle file creation events by adding new inodes to the map + // Handle file and directory creation events by adding new inodes to the map if event.is_creation() && let Err(e) = self.handle_creation_event(&event) { warn!("Failed to handle creation event: {e}"); @@ -302,7 +302,7 @@ impl HostScanner { } // Skip directory creation events - we track them internally but don't send to sensor - if event.event_type() != fact_ebpf::file_activity_type_t::DIR_ACTIVITY_CREATION { + if !event.is_mkdir() { let event = Arc::new(event); if let Err(e) = self.tx.send(event) { self.metrics.events.dropped(); From db4645b1fea63d199342c14f59e2ff2fe94b80a2 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 13:43:08 -0700 Subject: [PATCH 27/32] make format --- fact/src/event/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 9f87430b..1854d7af 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -376,7 +376,9 @@ impl From for fact_api::file_activity::File { fact_api::file_activity::File::Creation(f_act) } FileData::MkDir(event) => { - warn!("MkDir event reached protobuf conversion - converting to Creation (filtering may have failed)"); + warn!( + "MkDir event reached protobuf conversion - converting to Creation (filtering may have failed)" + ); let activity = Some(fact_api::FileActivityBase::from(event)); let f_act = fact_api::FileCreation { activity }; fact_api::file_activity::File::Creation(f_act) From c4ef0acb506e2f32bef8bcfee66dbd7581f5a75a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 9 Apr 2026 20:54:02 -0700 Subject: [PATCH 28/32] Empty commit From 616720d691acf344a9ceeebe269efa0738889c69 Mon Sep 17 00:00:00 2001 From: Jouko Virtanen Date: Fri, 10 Apr 2026 07:47:59 -0700 Subject: [PATCH 29/32] Apply suggestions from code review Co-authored-by: Mauro Ezequiel Moltrasio --- fact/src/event/mod.rs | 10 +--------- fact/src/host_scanner.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 1854d7af..53d584a1 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -131,9 +131,6 @@ impl Event { matches!(self.file, FileData::Creation(_) | FileData::MkDir(_)) } - pub fn is_file_creation(&self) -> bool { - matches!(self.file, FileData::Creation(_)) - } pub fn is_mkdir(&self) -> bool { matches!(self.file, FileData::MkDir(_)) @@ -376,12 +373,7 @@ impl From for fact_api::file_activity::File { fact_api::file_activity::File::Creation(f_act) } FileData::MkDir(event) => { - warn!( - "MkDir event reached protobuf conversion - converting to Creation (filtering may have failed)" - ); - let activity = Some(fact_api::FileActivityBase::from(event)); - let f_act = fact_api::FileCreation { activity }; - fact_api::file_activity::File::Creation(f_act) + unreachable!("MkDir 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 f49c621f..7626c795 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -302,12 +302,15 @@ impl HostScanner { } // Skip directory creation events - we track them internally but don't send to sensor - if !event.is_mkdir() { - let event = Arc::new(event); - if let Err(e) = self.tx.send(event) { - self.metrics.events.dropped(); - warn!("Failed to send event: {e}"); - } + if event.is_mkdir() { + continue; + } + + let event = Arc::new(event); + if let Err(e) = self.tx.send(event) { + self.metrics.events.dropped(); + warn!("Failed to send event: {e}"); + } } }, _ = scan_trigger.notified() => self.scan()?, From c7ec07489e78b904a144db8f90667e46e2af5508 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 10 Apr 2026 09:39:02 -0700 Subject: [PATCH 30/32] Fixed syntax error --- fact/src/host_scanner.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 7626c795..9c5bf5e9 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -311,7 +311,6 @@ impl HostScanner { self.metrics.events.dropped(); warn!("Failed to send event: {e}"); } - } }, _ = scan_trigger.notified() => self.scan()?, _ = self.paths.changed() => self.scan()?, From 7ee6881efaa1da998266cee7c7b6818782c8a2a1 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 10 Apr 2026 09:39:58 -0700 Subject: [PATCH 31/32] make format --- fact/src/event/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 53d584a1..b5359161 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -131,7 +131,6 @@ impl Event { matches!(self.file, FileData::Creation(_) | FileData::MkDir(_)) } - pub fn is_mkdir(&self) -> bool { matches!(self.file, FileData::MkDir(_)) } From 4b77658837f196ab53fa536f24c3aa97b7fc625d Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 10 Apr 2026 09:54:16 -0700 Subject: [PATCH 32/32] Fixed clippy errors --- fact/src/event/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index b5359161..1c87a859 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -7,7 +7,6 @@ use std::{ }; use globset::GlobSet; -use log::warn; use serde::Serialize; use fact_ebpf::{PATH_MAX, event_t, file_activity_type_t, inode_key_t}; @@ -371,7 +370,7 @@ impl From for fact_api::file_activity::File { let f_act = fact_api::FileCreation { activity }; fact_api::file_activity::File::Creation(f_act) } - FileData::MkDir(event) => { + FileData::MkDir(_) => { unreachable!("MkDir event reached protobuf conversion"); } FileData::Unlink(event) => {