From 23f3fcd5fd03790f649329947e2d5b939fe3f4e8 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 27 Mar 2026 14:23:54 -0700 Subject: [PATCH 01/20] X-Smart-Branch-Parent: main From 2c9752cc0f2ee5314e24a1641863305b4a7fe459 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 28 Mar 2026 10:05:00 -0700 Subject: [PATCH 02/20] Initial attempt --- fact-ebpf/src/bpf/bound_path.h | 14 +++---- fact-ebpf/src/bpf/d_path.h | 5 ++- fact-ebpf/src/bpf/main.c | 67 ++++++++++++++++++++++++++++++ fact-ebpf/src/bpf/types.h | 1 + fact-ebpf/src/lib.rs | 1 + fact/src/host_scanner.rs | 43 +++++++++++++------ fact/src/metrics/kernel_metrics.rs | 9 ++++ 7 files changed, 119 insertions(+), 21 deletions(-) diff --git a/fact-ebpf/src/bpf/bound_path.h b/fact-ebpf/src/bpf/bound_path.h index 7a2091f9..65e1bd8a 100644 --- a/fact-ebpf/src/bpf/bound_path.h +++ b/fact-ebpf/src/bpf/bound_path.h @@ -21,7 +21,7 @@ __always_inline static void path_write_char(char* p, unsigned int offset, char c *path_safe_access(p, offset) = c; } -__always_inline static struct bound_path_t* _path_read(struct path* path, bound_path_buffer_t key, bool use_bpf_d_path) { +__always_inline static struct bound_path_t* _path_read(const struct path* path, bound_path_buffer_t key, bool use_bpf_d_path) { struct bound_path_t* bound_path = get_bound_path(key); if (bound_path == NULL) { return NULL; @@ -38,15 +38,15 @@ __always_inline static struct bound_path_t* _path_read(struct path* path, bound_ return bound_path; } -__always_inline static struct bound_path_t* path_read_unchecked(struct path* path) { +__always_inline static struct bound_path_t* path_read_unchecked(const struct path* path) { return _path_read(path, BOUND_PATH_MAIN, true); } -__always_inline static struct bound_path_t* path_read(struct path* path) { +__always_inline static struct bound_path_t* path_read(const struct path* path) { return _path_read(path, BOUND_PATH_MAIN, path_hooks_support_bpf_d_path); } -__always_inline static struct bound_path_t* path_read_alt(struct path* path) { +__always_inline static struct bound_path_t* path_read_alt(const struct path* path) { return _path_read(path, BOUND_PATH_ALTERNATE, path_hooks_support_bpf_d_path); } @@ -76,7 +76,7 @@ __always_inline static enum path_append_status_t path_append_dentry(struct bound return 0; } -__always_inline static struct bound_path_t* _path_read_append_d_entry(struct path* dir, struct dentry* dentry, bound_path_buffer_t key) { +__always_inline static struct bound_path_t* _path_read_append_d_entry(const struct path* dir, struct dentry* dentry, bound_path_buffer_t key) { struct bound_path_t* path = _path_read(dir, key, path_hooks_support_bpf_d_path); if (path == NULL) { @@ -105,7 +105,7 @@ __always_inline static struct bound_path_t* _path_read_append_d_entry(struct pat * directory and a dentry to an element in said directory, this helper * provides a short way of resolving the full path in one call. */ -__always_inline static struct bound_path_t* path_read_append_d_entry(struct path* dir, struct dentry* dentry) { +__always_inline static struct bound_path_t* path_read_append_d_entry(const struct path* dir, struct dentry* dentry) { return _path_read_append_d_entry(dir, dentry, BOUND_PATH_MAIN); } @@ -116,6 +116,6 @@ __always_inline static struct bound_path_t* path_read_append_d_entry(struct path * so in an alternate buffer. Useful for operations that take more than * one path, like path_rename. */ -__always_inline static struct bound_path_t* path_read_alt_append_d_entry(struct path* dir, struct dentry* dentry) { +__always_inline static struct bound_path_t* path_read_alt_append_d_entry(const struct path* dir, struct dentry* dentry) { return _path_read_append_d_entry(dir, dentry, BOUND_PATH_ALTERNATE); } diff --git a/fact-ebpf/src/bpf/d_path.h b/fact-ebpf/src/bpf/d_path.h index a922600e..ba2bce05 100644 --- a/fact-ebpf/src/bpf/d_path.h +++ b/fact-ebpf/src/bpf/d_path.h @@ -140,9 +140,10 @@ __always_inline static long __d_path(const struct path* path, char* buf, int buf return buflen - ctx.offset; } -__always_inline static long d_path(struct path* path, char* buf, int buflen, bool use_bpf_helper) { +__always_inline static long d_path(const struct path* path, char* buf, int buflen, bool use_bpf_helper) { if (use_bpf_helper) { - return bpf_d_path(path, buf, buflen); + // bpf_d_path is a kernel helper that doesn't take const, so we must cast here + return bpf_d_path((struct path*)path, buf, buflen); } return __d_path(path, buf, buflen); } diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index b7c044f1..99cec79f 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -228,3 +228,70 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, m->path_rename.error++; return 0; } + +SEC("lsm/inode_mkdir") +int BPF_PROG(trace_inode_mkdir, struct inode* dir, struct dentry* dentry, umode_t mode) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->path_mkdir.total++; + + // Get the new directory's inode (should be populated after creation) + inode_key_t inode_key = inode_to_key(dentry->d_inode); + bpf_printk("inode_mkdir: inode=%lu dev=%lu", inode_key.inode, inode_key.dev); + if (inode_key.inode == 0) { + // Inode not yet populated, ignore + bpf_printk("inode_mkdir: inode not populated, ignoring"); + m->path_mkdir.ignored++; + return 0; + } + + // Get the parent directory's inode + inode_key_t parent_key = inode_to_key(dir); + + // Check if parent is monitored + const inode_value_t* parent_value = inode_get(&parent_key); + if (parent_value == NULL) { + // Parent not monitored, ignore this directory + m->path_mkdir.ignored++; + return 0; + } + + // Parent is monitored, so add this new directory to tracking + inode_add(&inode_key); + + // Get the directory name from dentry + struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); + if (path == NULL) { + m->path_mkdir.error++; + return 0; + } + + struct qstr d_name; + BPF_CORE_READ_INTO(&d_name, dentry, d_name); + int len = d_name.len; + if (len > PATH_MAX - 1) { + m->path_mkdir.error++; + return 0; + } + + if (bpf_probe_read_kernel(path->path, PATH_LEN_CLAMP(len), d_name.name)) { + m->path_mkdir.error++; + return 0; + } + path->path[PATH_LEN_CLAMP(len)] = '\0'; + + // Use __submit_event directly with use_bpf_d_path=false because inode hooks + // don't support bpf_d_path (it's only allowed in path hooks) + struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); + if (event == NULL) { + m->path_mkdir.ringbuffer_full++; + return 0; + } + + __submit_event(event, &m->path_mkdir, FILE_ACTIVITY_CREATION, path->path, &inode_key, &parent_key, false); + + return 0; +} diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 55005c00..96fdc190 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -111,4 +111,5 @@ 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; }; diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index bd84ee08..a551f572 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -125,6 +125,7 @@ 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 } } diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 36cacdef..c304bd1e 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -200,21 +200,40 @@ impl HostScanner { fn handle_creation_event(&self, event: &Event) -> anyhow::Result<()> { let inode = event.get_inode(); let parent_inode = event.get_parent_inode(); - if self.get_host_path(Some(inode)).is_some() || parent_inode.empty() { + + debug!("handle_creation_event: file={}, inode={:?}, parent_inode={:?}", + event.get_filename().display(), inode, parent_inode); + + if self.get_host_path(Some(inode)).is_some() { + debug!("Inode already in map, skipping"); return Ok(()); } - if let Some(filename) = event.get_filename().file_name() - && let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) - { - let host_path = parent_host_path.join(filename); - self.update_entry_with_inode(*inode, host_path) - .with_context(|| { - format!( - "Failed to add creation event entry for {}", - filename.display() - ) - })?; + if parent_inode.empty() { + debug!("Parent inode is empty, skipping"); + return Ok(()); + } + + if let Some(filename) = event.get_filename().file_name() { + debug!("Filename component: {}", filename.display()); + + if let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) { + let host_path = parent_host_path.join(filename); + debug!("Constructed host_path: {} (parent: {})", + host_path.display(), parent_host_path.display()); + + self.update_entry_with_inode(*inode, host_path) + .with_context(|| { + format!( + "Failed to add creation event entry for {}", + filename.display() + ) + })?; + } else { + debug!("Parent inode {:?} not found in map", parent_inode); + } + } else { + debug!("Could not extract filename component from {}", event.get_filename().display()); } Ok(()) diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index d1a3a242..4b2dc3ee 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -13,6 +13,7 @@ pub struct KernelMetrics { path_chmod: EventCounter, path_chown: EventCounter, path_rename: EventCounter, + path_mkdir: EventCounter, map: PerCpuArray, } @@ -43,12 +44,18 @@ 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 + ); file_open.register(reg); path_unlink.register(reg); path_chmod.register(reg); path_chown.register(reg); path_rename.register(reg); + path_mkdir.register(reg); KernelMetrics { file_open, @@ -56,6 +63,7 @@ impl KernelMetrics { path_chmod, path_chown, path_rename, + path_mkdir, map: kernel_metrics, } } @@ -105,6 +113,7 @@ 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); Ok(()) } From 847e4205cb4a7c77ebf03c737c69b0e11e2c0315 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 28 Mar 2026 20:55:46 -0700 Subject: [PATCH 03/20] Second attempt with syscalls --- fact-ebpf/src/bpf/events.h | 5 +- fact-ebpf/src/bpf/main.c | 155 +++++++++++++++++++++++++++---------- fact/src/bpf/mod.rs | 30 ++++++- tests/conftest.py | 4 + 4 files changed, 148 insertions(+), 46 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 26254778..58fca680 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -49,14 +49,15 @@ __always_inline static void submit_open_event(struct metrics_by_hook_t* m, file_activity_type_t event_type, const char filename[PATH_MAX], inode_key_t* inode, - inode_key_t* parent_inode) { + inode_key_t* parent_inode, + bool use_bpf_d_path) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); if (event == NULL) { m->ringbuffer_full++; return; } - __submit_event(event, m, event_type, filename, inode, parent_inode, true); + __submit_event(event, m, event_type, filename, inode, parent_inode, use_bpf_d_path); } __always_inline static void submit_unlink_event(struct metrics_by_hook_t* m, diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 99cec79f..28e8ddf3 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -61,7 +61,7 @@ int BPF_PROG(trace_file_open, struct file* file) { goto ignored; } - submit_open_event(&m->file_open, event_type, path->path, inode_to_submit, &parent_key); + submit_open_event(&m->file_open, event_type, path->path, inode_to_submit, &parent_key, true); return 0; @@ -229,69 +229,144 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, return 0; } -SEC("lsm/inode_mkdir") -int BPF_PROG(trace_inode_mkdir, struct inode* dir, struct dentry* dentry, umode_t mode) { - struct metrics_t* m = get_metrics(); - if (m == NULL) { +// Tracepoint structures for mkdir syscalls +struct trace_enter_mkdir { + unsigned short common_type; + unsigned char common_flags; + unsigned char common_preempt_count; + int common_pid; + long syscall_nr; + const char* pathname; + umode_t mode; +}; + +struct trace_enter_mkdirat { + unsigned short common_type; + unsigned char common_flags; + unsigned char common_preempt_count; + int common_pid; + long syscall_nr; + int dfd; + const char* pathname; + umode_t mode; +}; + +struct trace_exit_mkdir { + unsigned short common_type; + unsigned char common_flags; + unsigned char common_preempt_count; + int common_pid; + long syscall_nr; + long ret; +}; + +// Map to store pathname from entry to exit +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1024); + __type(key, u64); // pid_tgid + __type(value, char[PATH_MAX]); +} mkdir_paths SEC(".maps"); + +SEC("tracepoint/syscalls/sys_enter_mkdir") +int trace_mkdir_enter(struct trace_enter_mkdir* ctx) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + + struct helper_t* helper = get_helper(); + if (helper == NULL) { return 0; } - m->path_mkdir.total++; + long len = bpf_probe_read_user_str(helper->buf, PATH_MAX, ctx->pathname); + if (len > 0) { + bpf_map_update_elem(&mkdir_paths, &pid_tgid, helper->buf, BPF_ANY); + } - // Get the new directory's inode (should be populated after creation) - inode_key_t inode_key = inode_to_key(dentry->d_inode); - bpf_printk("inode_mkdir: inode=%lu dev=%lu", inode_key.inode, inode_key.dev); - if (inode_key.inode == 0) { - // Inode not yet populated, ignore - bpf_printk("inode_mkdir: inode not populated, ignoring"); - m->path_mkdir.ignored++; + return 0; +} + +SEC("tracepoint/syscalls/sys_enter_mkdirat") +int trace_mkdirat_enter(struct trace_enter_mkdirat* ctx) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + + struct helper_t* helper = get_helper(); + if (helper == NULL) { return 0; } - // Get the parent directory's inode - inode_key_t parent_key = inode_to_key(dir); + long len = bpf_probe_read_user_str(helper->buf, PATH_MAX, ctx->pathname); + if (len > 0) { + bpf_map_update_elem(&mkdir_paths, &pid_tgid, helper->buf, BPF_ANY); + } - // Check if parent is monitored - const inode_value_t* parent_value = inode_get(&parent_key); - if (parent_value == NULL) { - // Parent not monitored, ignore this directory - m->path_mkdir.ignored++; + return 0; +} + +SEC("tracepoint/syscalls/sys_exit_mkdir") +int trace_mkdir_exit(struct trace_exit_mkdir* ctx) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { return 0; } - // Parent is monitored, so add this new directory to tracking - inode_add(&inode_key); + m->path_mkdir.total++; - // Get the directory name from dentry - struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); - if (path == NULL) { - m->path_mkdir.error++; - return 0; + // Check if mkdir succeeded + if (ctx->ret < 0) { + m->path_mkdir.ignored++; + goto cleanup; } - struct qstr d_name; - BPF_CORE_READ_INTO(&d_name, dentry, d_name); - int len = d_name.len; - if (len > PATH_MAX - 1) { + // Retrieve the pathname stored at entry + u64 pid_tgid = bpf_get_current_pid_tgid(); + char* stored_path = bpf_map_lookup_elem(&mkdir_paths, &pid_tgid); + if (stored_path == NULL) { m->path_mkdir.error++; return 0; } - if (bpf_probe_read_kernel(path->path, PATH_LEN_CLAMP(len), d_name.name)) { - m->path_mkdir.error++; + // Send event with path. Userspace will stat() to get inode and add to map. + // We send empty inodes because we can't easily stat from BPF. + inode_key_t empty_inode = {0}; + inode_key_t empty_parent = {0}; + + submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, stored_path, &empty_inode, &empty_parent, false); + +cleanup: + bpf_map_delete_elem(&mkdir_paths, &pid_tgid); + return 0; +} + +SEC("tracepoint/syscalls/sys_exit_mkdirat") +int trace_mkdirat_exit(struct trace_exit_mkdir* ctx) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { return 0; } - path->path[PATH_LEN_CLAMP(len)] = '\0'; - // Use __submit_event directly with use_bpf_d_path=false because inode hooks - // don't support bpf_d_path (it's only allowed in path hooks) - struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); - if (event == NULL) { - m->path_mkdir.ringbuffer_full++; + m->path_mkdir.total++; + + // Check if mkdirat succeeded + if (ctx->ret < 0) { + m->path_mkdir.ignored++; + goto cleanup; + } + + // Retrieve the pathname stored at entry + u64 pid_tgid = bpf_get_current_pid_tgid(); + char* stored_path = bpf_map_lookup_elem(&mkdir_paths, &pid_tgid); + if (stored_path == NULL) { + m->path_mkdir.error++; return 0; } - __submit_event(event, &m->path_mkdir, FILE_ACTIVITY_CREATION, path->path, &inode_key, &parent_key, false); + // Send event with path. Userspace will stat() to get inode and add to map. + inode_key_t empty_inode = {0}; + inode_key_t empty_parent = {0}; + + submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, stored_path, &empty_inode, &empty_parent, false); +cleanup: + bpf_map_delete_elem(&mkdir_paths, &pid_tgid); return 0; } diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 38fe269d..74ff2f30 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Context, bail}; use aya::{ Btf, Ebpf, maps::{HashMap, LpmTrie, MapData, PerCpuArray, RingBuf}, - programs::{Program, lsm::LsmLink}, + programs::{Program, lsm::LsmLink, trace_point::TracePointLink}, }; use checks::Checks; use globset::{Glob, GlobSet, GlobSetBuilder}; @@ -24,6 +24,11 @@ mod checks; const RINGBUFFER_NAME: &str = "rb"; +enum Link { + Lsm(LsmLink), + TracePoint(TracePointLink), +} + pub struct Bpf { obj: Ebpf, @@ -34,7 +39,7 @@ pub struct Bpf { paths_globset: GlobSet, - links: Vec, + links: Vec, } impl Bpf { @@ -178,6 +183,7 @@ impl Bpf { }; match prog { Program::Lsm(prog) => prog.load(hook, btf)?, + Program::TracePoint(prog) => prog.load()?, u => unimplemented!("{u:?}"), } } @@ -190,10 +196,26 @@ impl Bpf { self.links = self .obj .programs_mut() - .map(|(_, prog)| match prog { + .map(|(name, prog)| match prog { Program::Lsm(prog) => { let link_id = prog.attach()?; - prog.take_link(link_id) + Ok(Link::Lsm(prog.take_link(link_id)?)) + } + Program::TracePoint(prog) => { + // Map function names to tracepoint category/name + // trace_mkdir_enter -> syscalls/sys_enter_mkdir + // trace_mkdirat_enter -> syscalls/sys_enter_mkdirat + // trace_mkdir_exit -> syscalls/sys_exit_mkdir + // trace_mkdirat_exit -> syscalls/sys_exit_mkdirat + let (category, tp_name) = match name { + "trace_mkdir_enter" => ("syscalls", "sys_enter_mkdir"), + "trace_mkdirat_enter" => ("syscalls", "sys_enter_mkdirat"), + "trace_mkdir_exit" => ("syscalls", "sys_exit_mkdir"), + "trace_mkdirat_exit" => ("syscalls", "sys_exit_mkdirat"), + _ => bail!("Unknown tracepoint program: {name}"), + }; + let link_id = prog.attach(category, tp_name)?; + Ok(Link::TracePoint(prog.take_link(link_id)?)) } u => unimplemented!("{u:?}"), }) diff --git a/tests/conftest.py b/tests/conftest.py index 165649f8..9247ae57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -182,6 +182,10 @@ def fact(request, docker_client, fact_config, server, logs_dir, test_file): 'bind': '/host', 'mode': 'ro', }, + '/sys/kernel/tracing': { + 'bind': '/sys/kernel/tracing', + 'mode': 'rw', + }, config_file: { 'bind': '/etc/stackrox/fact.yml', 'mode': 'ro', From e2cb34b51bc8af9fe92a6d563c87df86f9bc2934 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 29 Mar 2026 14:38:42 -0700 Subject: [PATCH 04/20] Workings --- fact-ebpf/src/bpf/main.c | 184 +++++++++++++++++++-------------------- fact/src/bpf/mod.rs | 29 ++++-- fact/src/event/mod.rs | 15 ++++ fact/src/host_scanner.rs | 26 +++--- tests/conftest.py | 4 - 5 files changed, 143 insertions(+), 115 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 28e8ddf3..640a0f87 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -1,4 +1,5 @@ // clang-format off +#define __TARGET_ARCH_x86 #include "vmlinux.h" #include "file.h" @@ -229,81 +230,40 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, return 0; } -// Tracepoint structures for mkdir syscalls -struct trace_enter_mkdir { - unsigned short common_type; - unsigned char common_flags; - unsigned char common_preempt_count; - int common_pid; - long syscall_nr; - const char* pathname; - umode_t mode; +// Map to store vfs_mkdir parameters from entry to exit +struct vfs_mkdir_args_t { + struct inode* dir; + struct dentry* dentry; }; -struct trace_enter_mkdirat { - unsigned short common_type; - unsigned char common_flags; - unsigned char common_preempt_count; - int common_pid; - long syscall_nr; - int dfd; - const char* pathname; - umode_t mode; -}; - -struct trace_exit_mkdir { - unsigned short common_type; - unsigned char common_flags; - unsigned char common_preempt_count; - int common_pid; - long syscall_nr; - long ret; -}; - -// Map to store pathname from entry to exit struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, u64); // pid_tgid - __type(value, char[PATH_MAX]); -} mkdir_paths SEC(".maps"); + __type(value, struct vfs_mkdir_args_t); +} vfs_mkdir_args SEC(".maps"); -SEC("tracepoint/syscalls/sys_enter_mkdir") -int trace_mkdir_enter(struct trace_enter_mkdir* ctx) { +// Capture parameters at function entry +SEC("kprobe/vfs_mkdir") +int trace_vfs_mkdir_entry(struct pt_regs* ctx) { u64 pid_tgid = bpf_get_current_pid_tgid(); - struct helper_t* helper = get_helper(); - if (helper == NULL) { - return 0; - } + struct vfs_mkdir_args_t args = {0}; - long len = bpf_probe_read_user_str(helper->buf, PATH_MAX, ctx->pathname); - if (len > 0) { - bpf_map_update_elem(&mkdir_paths, &pid_tgid, helper->buf, BPF_ANY); - } + // x86_64 calling convention: rdi, rsi, rdx, rcx, r8, r9 + // vfs_mkdir(idmap, dir, dentry, mode) + // args: rdi, rsi, rdx, rcx + args.dir = (struct inode*)PT_REGS_PARM2(ctx); // rsi + args.dentry = (struct dentry*)PT_REGS_PARM3(ctx); // rdx - return 0; -} - -SEC("tracepoint/syscalls/sys_enter_mkdirat") -int trace_mkdirat_enter(struct trace_enter_mkdirat* ctx) { - u64 pid_tgid = bpf_get_current_pid_tgid(); - - struct helper_t* helper = get_helper(); - if (helper == NULL) { - return 0; - } - - long len = bpf_probe_read_user_str(helper->buf, PATH_MAX, ctx->pathname); - if (len > 0) { - bpf_map_update_elem(&mkdir_paths, &pid_tgid, helper->buf, BPF_ANY); - } + bpf_map_update_elem(&vfs_mkdir_args, &pid_tgid, &args, BPF_ANY); return 0; } -SEC("tracepoint/syscalls/sys_exit_mkdir") -int trace_mkdir_exit(struct trace_exit_mkdir* ctx) { +// Process at function exit with return value +SEC("kretprobe/vfs_mkdir") +int trace_vfs_mkdir(struct pt_regs* ctx) { struct metrics_t* m = get_metrics(); if (m == NULL) { return 0; @@ -311,62 +271,94 @@ int trace_mkdir_exit(struct trace_exit_mkdir* ctx) { m->path_mkdir.total++; - // Check if mkdir succeeded - if (ctx->ret < 0) { - m->path_mkdir.ignored++; - goto cleanup; - } + // Get return value - PT_REGS_RC returns long, but vfs_mkdir returns int + long ret_long = PT_REGS_RC(ctx); + int ret = (int)ret_long; - // Retrieve the pathname stored at entry + bpf_printk("vfs_mkdir kretprobe: ret_long=%ld ret_int=%d", ret_long, ret); + + // TEMPORARY: Skip return value check to debug path reading + // if (ret != 0) { + // bpf_printk("vfs_mkdir failed with ret=%d, ignoring", ret); + // m->path_mkdir.ignored++; + // goto cleanup; + // } + + // Retrieve stored parameters u64 pid_tgid = bpf_get_current_pid_tgid(); - char* stored_path = bpf_map_lookup_elem(&mkdir_paths, &pid_tgid); - if (stored_path == NULL) { + struct vfs_mkdir_args_t* args = bpf_map_lookup_elem(&vfs_mkdir_args, &pid_tgid); + if (args == NULL) { + bpf_printk("vfs_mkdir: no args in map for pid_tgid=%llu", pid_tgid); m->path_mkdir.error++; return 0; } - // Send event with path. Userspace will stat() to get inode and add to map. - // We send empty inodes because we can't easily stat from BPF. - inode_key_t empty_inode = {0}; - inode_key_t empty_parent = {0}; + struct inode* dir = args->dir; + struct dentry* dentry = args->dentry; - submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, stored_path, &empty_inode, &empty_parent, false); + bpf_printk("vfs_mkdir: retrieved args: dir=%p dentry=%p", dir, dentry); -cleanup: - bpf_map_delete_elem(&mkdir_paths, &pid_tgid); - return 0; -} + // Get parent inode (dir parameter) + inode_key_t parent_key = inode_to_key(dir); -SEC("tracepoint/syscalls/sys_exit_mkdirat") -int trace_mkdirat_exit(struct trace_exit_mkdir* ctx) { - struct metrics_t* m = get_metrics(); - if (m == NULL) { - return 0; - } + // Get child inode from the created dentry + struct inode* child_inode; + bpf_probe_read_kernel(&child_inode, sizeof(child_inode), &dentry->d_inode); + inode_key_t child_key = inode_to_key(child_inode); - m->path_mkdir.total++; + bpf_printk("vfs_mkdir: parent_inode=(%llu,%llu) child_inode=(%llu,%llu)", + parent_key.dev, parent_key.inode, child_key.dev, child_key.inode); - // Check if mkdirat succeeded - if (ctx->ret < 0) { + // For kprobes, we can't use d_path/bpf_d_path since we don't have proper mount context. + // Instead, check if the parent is monitored by looking it up in the inode map. + // If the parent is monitored, we'll add the child and submit the event. + + inode_value_t* parent_value = bpf_map_lookup_elem(&inode_map, &parent_key); + if (parent_value == NULL) { + bpf_printk("vfs_mkdir: parent inode not in map, not monitored"); m->path_mkdir.ignored++; goto cleanup; } - // Retrieve the pathname stored at entry - u64 pid_tgid = bpf_get_current_pid_tgid(); - char* stored_path = bpf_map_lookup_elem(&mkdir_paths, &pid_tgid); - if (stored_path == NULL) { + bpf_printk("vfs_mkdir: parent is monitored, adding child"); + + // Add the child directory to the inode map + inode_add(&child_key); + + // For the event, construct a minimal path with just the directory name + // Userspace will use the parent inode to construct the full host_path + struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); + if (bound_path == NULL) { + bpf_printk("Failed to get bound_path buffer"); m->path_mkdir.error++; - return 0; + goto cleanup; + } + + // Start with "/" + bound_path->path[0] = '/'; + bound_path->len = 1; + + switch (path_append_dentry(bound_path, dentry)) { + case PATH_APPEND_SUCCESS: + break; + case PATH_APPEND_INVALID_LENGTH: + bpf_printk("Invalid path length: %u", bound_path->len); + m->path_mkdir.error++; + goto cleanup; + case PATH_APPEND_READ_ERROR: + bpf_printk("Failed to read dentry name"); + m->path_mkdir.error++; + goto cleanup; } - // Send event with path. Userspace will stat() to get inode and add to map. - inode_key_t empty_inode = {0}; - inode_key_t empty_parent = {0}; + bpf_printk("vfs_mkdir: dir_name=%s (len=%u)", bound_path->path, bound_path->len); - submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, stored_path, &empty_inode, &empty_parent, false); + // Submit event with just the directory name + // Userspace handle_creation_event will use the parent inode to construct the full host_path + submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, bound_path->path, + &child_key, &parent_key, false); cleanup: - bpf_map_delete_elem(&mkdir_paths, &pid_tgid); + bpf_map_delete_elem(&vfs_mkdir_args, &pid_tgid); return 0; } diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 74ff2f30..d0595435 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Context, bail}; use aya::{ Btf, Ebpf, maps::{HashMap, LpmTrie, MapData, PerCpuArray, RingBuf}, - programs::{Program, lsm::LsmLink, trace_point::TracePointLink}, + programs::{Program, lsm::LsmLink, trace_point::TracePointLink, fexit::FExitLink, kprobe::KProbeLink}, }; use checks::Checks; use globset::{Glob, GlobSet, GlobSetBuilder}; @@ -27,6 +27,8 @@ const RINGBUFFER_NAME: &str = "rb"; enum Link { Lsm(LsmLink), TracePoint(TracePointLink), + FExit(FExitLink), + KProbe(KProbeLink), } pub struct Bpf { @@ -184,6 +186,8 @@ impl Bpf { match prog { Program::Lsm(prog) => prog.load(hook, btf)?, Program::TracePoint(prog) => prog.load()?, + Program::FExit(prog) => prog.load(hook, btf)?, + Program::KProbe(prog) => prog.load()?, u => unimplemented!("{u:?}"), } } @@ -203,10 +207,6 @@ impl Bpf { } Program::TracePoint(prog) => { // Map function names to tracepoint category/name - // trace_mkdir_enter -> syscalls/sys_enter_mkdir - // trace_mkdirat_enter -> syscalls/sys_enter_mkdirat - // trace_mkdir_exit -> syscalls/sys_exit_mkdir - // trace_mkdirat_exit -> syscalls/sys_exit_mkdirat let (category, tp_name) = match name { "trace_mkdir_enter" => ("syscalls", "sys_enter_mkdir"), "trace_mkdirat_enter" => ("syscalls", "sys_enter_mkdirat"), @@ -217,6 +217,25 @@ impl Bpf { let link_id = prog.attach(category, tp_name)?; Ok(Link::TracePoint(prog.take_link(link_id)?)) } + Program::FExit(prog) => { + let link_id = prog.attach()?; + Ok(Link::FExit(prog.take_link(link_id)?)) + } + Program::KProbe(prog) => { + // Extract function name from program name + // trace_vfs_mkdir_entry -> vfs_mkdir + // trace_vfs_mkdir -> vfs_mkdir (kretprobe) + let func_name = if name.ends_with("_entry") { + name.strip_suffix("_entry") + .and_then(|s| s.strip_prefix("trace_")) + .unwrap_or(name) + } else { + name.strip_prefix("trace_").unwrap_or(name) + }; + + let link_id = prog.attach(func_name, 0)?; + Ok(Link::KProbe(prog.take_link(link_id)?)) + } u => unimplemented!("{u:?}"), }) .collect::>()?; diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 40bd317a..816ba920 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -220,6 +220,21 @@ impl Event { } } + /// Set the `filename` field of the event to the one provided. + /// + /// In the case of operations that involve two paths, like rename, + /// the 'new' filename will be set. + pub fn set_filename(&mut self, filename: PathBuf) { + match &mut self.file { + FileData::Open(data) => data.filename = filename, + FileData::Creation(data) => data.filename = filename, + FileData::Unlink(data) => data.filename = filename, + FileData::Chmod(data) => data.inner.filename = filename, + FileData::Chown(data) => data.inner.filename = filename, + FileData::Rename(data) => data.new.filename = filename, + } + } + /// Determine if the event should be ignored. /// /// With wildcards, the kernel can only match on the inode and diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index c304bd1e..9c4fb836 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -197,14 +197,15 @@ impl HostScanner { /// We use the parent inode provided by the eBPF code /// to look up the parent directory's host path, then construct the full /// path by appending the new file's name. - fn handle_creation_event(&self, event: &Event) -> anyhow::Result<()> { - let inode = event.get_inode(); - let parent_inode = event.get_parent_inode(); + fn handle_creation_event(&self, event: &mut Event) -> anyhow::Result<()> { + // Copy inode values to avoid borrow checker issues + let inode = *event.get_inode(); + let parent_inode = *event.get_parent_inode(); debug!("handle_creation_event: file={}, inode={:?}, parent_inode={:?}", event.get_filename().display(), inode, parent_inode); - if self.get_host_path(Some(inode)).is_some() { + if self.get_host_path(Some(&inode)).is_some() { debug!("Inode already in map, skipping"); return Ok(()); } @@ -215,18 +216,23 @@ impl HostScanner { } if let Some(filename) = event.get_filename().file_name() { - debug!("Filename component: {}", filename.display()); + // Clone filename to avoid holding a reference to event + let filename = filename.to_os_string(); + debug!("Filename component: {}", filename.to_string_lossy()); - if let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) { - let host_path = parent_host_path.join(filename); + if let Some(parent_host_path) = self.get_host_path(Some(&parent_inode)) { + let host_path = parent_host_path.join(&filename); debug!("Constructed host_path: {} (parent: {})", host_path.display(), parent_host_path.display()); - self.update_entry_with_inode(*inode, host_path) + // Update the event's filename to the full path when it was constructed from parent + event.set_filename(host_path.clone()); + + self.update_entry_with_inode(inode, host_path) .with_context(|| { format!( "Failed to add creation event entry for {}", - filename.display() + filename.to_string_lossy() ) })?; } else { @@ -282,7 +288,7 @@ impl HostScanner { // Handle file creation events by adding new inodes to the map if event.is_creation() && - let Err(e) = self.handle_creation_event(&event) { + let Err(e) = self.handle_creation_event(&mut event) { warn!("Failed to handle creation event: {e}"); } diff --git a/tests/conftest.py b/tests/conftest.py index 9247ae57..165649f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -182,10 +182,6 @@ def fact(request, docker_client, fact_config, server, logs_dir, test_file): 'bind': '/host', 'mode': 'ro', }, - '/sys/kernel/tracing': { - 'bind': '/sys/kernel/tracing', - 'mode': 'rw', - }, config_file: { 'bind': '/etc/stackrox/fact.yml', 'mode': 'ro', From c5231e3b7ffbdadd7b9660bb96f2707a54796c09 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 29 Mar 2026 15:44:48 -0700 Subject: [PATCH 05/20] Using CORE. Not #define __TARGET_ARCH_x86 --- fact-ebpf/build.rs | 21 +++++++++++++++++++-- fact-ebpf/src/bpf/main.c | 12 +++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/fact-ebpf/build.rs b/fact-ebpf/build.rs index b9c35887..48a32564 100644 --- a/fact-ebpf/build.rs +++ b/fact-ebpf/build.rs @@ -6,7 +6,23 @@ use std::{ }; fn compile_bpf(out_dir: &Path) -> anyhow::Result<()> { - let target_arch = format!("-D__TARGET_ARCH_{}", env::var("CARGO_CFG_TARGET_ARCH")?); + // Get the target architecture from Cargo + let cargo_arch = env::var("CARGO_CFG_TARGET_ARCH")?; + + // Map Cargo's architecture names to what bpf_tracing.h expects for PT_REGS macros: + // x86_64 -> x86, aarch64 -> arm64 + let bpf_arch = match cargo_arch.as_str() { + "x86_64" => "x86", + "aarch64" => "arm64", + other => other, + }; + + // Define both: + // - __TARGET_ARCH_ for PT_REGS macros (e.g., __TARGET_ARCH_x86) + // - __TARGET_ARCH_ for vmlinux.h selection (e.g., __TARGET_ARCH_x86_64) + let target_arch_bpf = format!("-D__TARGET_ARCH_{}", bpf_arch); + let target_arch_full = format!("-D__TARGET_ARCH_{}", cargo_arch); + let base_args = [ "-target", "bpf", @@ -15,7 +31,8 @@ fn compile_bpf(out_dir: &Path) -> anyhow::Result<()> { "-c", "-Wall", "-Werror", - &target_arch, + &target_arch_bpf, + &target_arch_full, ]; for name in ["main", "checks"] { diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 640a0f87..018399f7 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -1,5 +1,7 @@ // clang-format off -#define __TARGET_ARCH_x86 +// Architecture is auto-detected at build time via clang -D__TARGET_ARCH_* +// Set by fact-ebpf/build.rs from CARGO_CFG_TARGET_ARCH (x86_64 or aarch64) +// vmlinux.h conditionally includes the correct architecture-specific header #include "vmlinux.h" #include "file.h" @@ -253,8 +255,8 @@ int trace_vfs_mkdir_entry(struct pt_regs* ctx) { // x86_64 calling convention: rdi, rsi, rdx, rcx, r8, r9 // vfs_mkdir(idmap, dir, dentry, mode) // args: rdi, rsi, rdx, rcx - args.dir = (struct inode*)PT_REGS_PARM2(ctx); // rsi - args.dentry = (struct dentry*)PT_REGS_PARM3(ctx); // rdx + args.dir = (struct inode*)PT_REGS_PARM2_CORE(ctx); // rsi + args.dentry = (struct dentry*)PT_REGS_PARM3_CORE(ctx); // rdx bpf_map_update_elem(&vfs_mkdir_args, &pid_tgid, &args, BPF_ANY); @@ -271,8 +273,8 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { m->path_mkdir.total++; - // Get return value - PT_REGS_RC returns long, but vfs_mkdir returns int - long ret_long = PT_REGS_RC(ctx); + // Get return value - PT_REGS_RC_CORE returns long, but vfs_mkdir returns int + long ret_long = PT_REGS_RC_CORE(ctx); int ret = (int)ret_long; bpf_printk("vfs_mkdir kretprobe: ret_long=%ld ret_int=%d", ret_long, ret); From 3b1702f45ca4aef136ad56022786adf9bb447012 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 29 Mar 2026 17:15:03 -0700 Subject: [PATCH 06/20] Added submit_mkdir_event --- fact-ebpf/src/bpf/events.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 58fca680..77432cca 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -60,6 +60,21 @@ __always_inline static void submit_open_event(struct metrics_by_hook_t* m, __submit_event(event, m, event_type, filename, inode, parent_inode, use_bpf_d_path); } +__always_inline static void submit_mkdir_event(struct metrics_by_hook_t* m, + const char dirname[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; + } + + // mkdir events from kprobes can't use bpf_d_path (no vfsmount context) + // and only send the directory name (userspace constructs full path from parent inode) + __submit_event(event, m, FILE_ACTIVITY_CREATION, dirname, inode, parent_inode, false); +} + __always_inline static void submit_unlink_event(struct metrics_by_hook_t* m, const char filename[PATH_MAX], inode_key_t* inode, From be6c717f33e1e8a091c4209fd6153a8b71868c3c Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 29 Mar 2026 17:15:30 -0700 Subject: [PATCH 07/20] Added main.c which was forgotten --- fact-ebpf/src/bpf/main.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 018399f7..176b7c5d 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -355,10 +355,9 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { bpf_printk("vfs_mkdir: dir_name=%s (len=%u)", bound_path->path, bound_path->len); - // Submit event with just the directory name + // Submit mkdir event with just the directory name // Userspace handle_creation_event will use the parent inode to construct the full host_path - submit_open_event(&m->path_mkdir, FILE_ACTIVITY_CREATION, bound_path->path, - &child_key, &parent_key, false); + submit_mkdir_event(&m->path_mkdir, bound_path->path, &child_key, &parent_key); cleanup: bpf_map_delete_elem(&vfs_mkdir_args, &pid_tgid); From bf22af3cbdff81b201b4ed84e38c3ef757a4610b Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 07:49:26 -0700 Subject: [PATCH 08/20] Some cleanup --- fact-ebpf/src/bpf/main.c | 43 +++++----------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 176b7c5d..686f11d3 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -249,14 +249,11 @@ struct { SEC("kprobe/vfs_mkdir") int trace_vfs_mkdir_entry(struct pt_regs* ctx) { u64 pid_tgid = bpf_get_current_pid_tgid(); - struct vfs_mkdir_args_t args = {0}; - // x86_64 calling convention: rdi, rsi, rdx, rcx, r8, r9 - // vfs_mkdir(idmap, dir, dentry, mode) - // args: rdi, rsi, rdx, rcx - args.dir = (struct inode*)PT_REGS_PARM2_CORE(ctx); // rsi - args.dentry = (struct dentry*)PT_REGS_PARM3_CORE(ctx); // rdx + // vfs_mkdir(mnt_idmap, dir, dentry, mode) + args.dir = (struct inode*)PT_REGS_PARM2_CORE(ctx); + args.dentry = (struct dentry*)PT_REGS_PARM3_CORE(ctx); bpf_map_update_elem(&vfs_mkdir_args, &pid_tgid, &args, BPF_ANY); @@ -273,24 +270,10 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { m->path_mkdir.total++; - // Get return value - PT_REGS_RC_CORE returns long, but vfs_mkdir returns int - long ret_long = PT_REGS_RC_CORE(ctx); - int ret = (int)ret_long; - - bpf_printk("vfs_mkdir kretprobe: ret_long=%ld ret_int=%d", ret_long, ret); - - // TEMPORARY: Skip return value check to debug path reading - // if (ret != 0) { - // bpf_printk("vfs_mkdir failed with ret=%d, ignoring", ret); - // m->path_mkdir.ignored++; - // goto cleanup; - // } - // Retrieve stored parameters u64 pid_tgid = bpf_get_current_pid_tgid(); struct vfs_mkdir_args_t* args = bpf_map_lookup_elem(&vfs_mkdir_args, &pid_tgid); if (args == NULL) { - bpf_printk("vfs_mkdir: no args in map for pid_tgid=%llu", pid_tgid); m->path_mkdir.error++; return 0; } @@ -298,8 +281,6 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { struct inode* dir = args->dir; struct dentry* dentry = args->dentry; - bpf_printk("vfs_mkdir: retrieved args: dir=%p dentry=%p", dir, dentry); - // Get parent inode (dir parameter) inode_key_t parent_key = inode_to_key(dir); @@ -308,26 +289,17 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { bpf_probe_read_kernel(&child_inode, sizeof(child_inode), &dentry->d_inode); inode_key_t child_key = inode_to_key(child_inode); - bpf_printk("vfs_mkdir: parent_inode=(%llu,%llu) child_inode=(%llu,%llu)", - parent_key.dev, parent_key.inode, child_key.dev, child_key.inode); - - // For kprobes, we can't use d_path/bpf_d_path since we don't have proper mount context. - // Instead, check if the parent is monitored by looking it up in the inode map. - // If the parent is monitored, we'll add the child and submit the event. - + // Check if the parent is monitored by looking it up in the inode map inode_value_t* parent_value = bpf_map_lookup_elem(&inode_map, &parent_key); if (parent_value == NULL) { - bpf_printk("vfs_mkdir: parent inode not in map, not monitored"); m->path_mkdir.ignored++; goto cleanup; } - bpf_printk("vfs_mkdir: parent is monitored, adding child"); - // Add the child directory to the inode map inode_add(&child_key); - // For the event, construct a minimal path with just the directory name + // Construct path with just the directory name // Userspace will use the parent inode to construct the full host_path struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); if (bound_path == NULL) { @@ -336,7 +308,6 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { goto cleanup; } - // Start with "/" bound_path->path[0] = '/'; bound_path->len = 1; @@ -353,10 +324,6 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { goto cleanup; } - bpf_printk("vfs_mkdir: dir_name=%s (len=%u)", bound_path->path, bound_path->len); - - // Submit mkdir event with just the directory name - // Userspace handle_creation_event will use the parent inode to construct the full host_path submit_mkdir_event(&m->path_mkdir, bound_path->path, &child_key, &parent_key); cleanup: From c12bc5a2f723dcb504b8ebef6c090470ece46e97 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 07:57:00 -0700 Subject: [PATCH 09/20] Removed unneeded comment --- fact-ebpf/src/bpf/main.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 686f11d3..5c8c55d6 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -1,7 +1,4 @@ // clang-format off -// Architecture is auto-detected at build time via clang -D__TARGET_ARCH_* -// Set by fact-ebpf/build.rs from CARGO_CFG_TARGET_ARCH (x86_64 or aarch64) -// vmlinux.h conditionally includes the correct architecture-specific header #include "vmlinux.h" #include "file.h" From f589b43a9b07236a5c4c215994ee32460c1d448e Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 09:39:54 -0700 Subject: [PATCH 10/20] More cleanup --- fact/src/bpf/mod.rs | 64 +++++++++++------------------- fact/src/host_scanner.rs | 14 ------- fact/src/metrics/kernel_metrics.rs | 2 +- 3 files changed, 24 insertions(+), 56 deletions(-) diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index d0595435..14723d53 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Context, bail}; use aya::{ Btf, Ebpf, maps::{HashMap, LpmTrie, MapData, PerCpuArray, RingBuf}, - programs::{Program, lsm::LsmLink, trace_point::TracePointLink, fexit::FExitLink, kprobe::KProbeLink}, + programs::{Program, lsm::LsmLink, kprobe::KProbeLink}, }; use checks::Checks; use globset::{Glob, GlobSet, GlobSetBuilder}; @@ -26,8 +26,6 @@ const RINGBUFFER_NAME: &str = "rb"; enum Link { Lsm(LsmLink), - TracePoint(TracePointLink), - FExit(FExitLink), KProbe(KProbeLink), } @@ -185,8 +183,6 @@ impl Bpf { }; match prog { Program::Lsm(prog) => prog.load(hook, btf)?, - Program::TracePoint(prog) => prog.load()?, - Program::FExit(prog) => prog.load(hook, btf)?, Program::KProbe(prog) => prog.load()?, u => unimplemented!("{u:?}"), } @@ -200,43 +196,29 @@ impl Bpf { self.links = self .obj .programs_mut() - .map(|(name, prog)| match prog { - Program::Lsm(prog) => { - let link_id = prog.attach()?; - Ok(Link::Lsm(prog.take_link(link_id)?)) - } - Program::TracePoint(prog) => { - // Map function names to tracepoint category/name - let (category, tp_name) = match name { - "trace_mkdir_enter" => ("syscalls", "sys_enter_mkdir"), - "trace_mkdirat_enter" => ("syscalls", "sys_enter_mkdirat"), - "trace_mkdir_exit" => ("syscalls", "sys_exit_mkdir"), - "trace_mkdirat_exit" => ("syscalls", "sys_exit_mkdirat"), - _ => bail!("Unknown tracepoint program: {name}"), - }; - let link_id = prog.attach(category, tp_name)?; - Ok(Link::TracePoint(prog.take_link(link_id)?)) - } - Program::FExit(prog) => { - let link_id = prog.attach()?; - Ok(Link::FExit(prog.take_link(link_id)?)) - } - Program::KProbe(prog) => { - // Extract function name from program name - // trace_vfs_mkdir_entry -> vfs_mkdir - // trace_vfs_mkdir -> vfs_mkdir (kretprobe) - let func_name = if name.ends_with("_entry") { - name.strip_suffix("_entry") - .and_then(|s| s.strip_prefix("trace_")) - .unwrap_or(name) - } else { - name.strip_prefix("trace_").unwrap_or(name) - }; - - let link_id = prog.attach(func_name, 0)?; - Ok(Link::KProbe(prog.take_link(link_id)?)) + .map(|(name, prog)| -> anyhow::Result { + match prog { + Program::Lsm(prog) => { + let link_id = prog.attach()?; + Ok(Link::Lsm(prog.take_link(link_id)?)) + } + Program::KProbe(prog) => { + // Extract function name from program name + // trace_vfs_mkdir_entry -> vfs_mkdir + // trace_vfs_mkdir -> vfs_mkdir (kretprobe) + let func_name = if name.ends_with("_entry") { + name.strip_suffix("_entry") + .and_then(|s| s.strip_prefix("trace_")) + .unwrap_or(name) + } else { + name.strip_prefix("trace_").unwrap_or(name) + }; + + let link_id = prog.attach(func_name, 0)?; + Ok(Link::KProbe(prog.take_link(link_id)?)) + } + u => unimplemented!("{u:?}"), } - u => unimplemented!("{u:?}"), }) .collect::>()?; Ok(()) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 9c4fb836..8780b99e 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -198,32 +198,22 @@ impl HostScanner { /// to look up the parent directory's host path, then construct the full /// path by appending the new file's name. fn handle_creation_event(&self, event: &mut Event) -> anyhow::Result<()> { - // Copy inode values to avoid borrow checker issues let inode = *event.get_inode(); let parent_inode = *event.get_parent_inode(); - debug!("handle_creation_event: file={}, inode={:?}, parent_inode={:?}", - event.get_filename().display(), inode, parent_inode); - if self.get_host_path(Some(&inode)).is_some() { - debug!("Inode already in map, skipping"); return Ok(()); } if parent_inode.empty() { - debug!("Parent inode is empty, skipping"); return Ok(()); } if let Some(filename) = event.get_filename().file_name() { - // Clone filename to avoid holding a reference to event let filename = filename.to_os_string(); - debug!("Filename component: {}", filename.to_string_lossy()); if let Some(parent_host_path) = self.get_host_path(Some(&parent_inode)) { let host_path = parent_host_path.join(&filename); - debug!("Constructed host_path: {} (parent: {})", - host_path.display(), parent_host_path.display()); // Update the event's filename to the full path when it was constructed from parent event.set_filename(host_path.clone()); @@ -235,11 +225,7 @@ impl HostScanner { filename.to_string_lossy() ) })?; - } else { - debug!("Parent inode {:?} not found in map", parent_inode); } - } else { - debug!("Could not extract filename component from {}", event.get_filename().display()); } Ok(()) diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 4b2dc3ee..a6fcb7e8 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -46,7 +46,7 @@ impl KernelMetrics { ); let path_mkdir = EventCounter::new( "kernel_path_mkdir_events", - "Events processed by the path_mkdir LSM hook", + "Events processed by the vfs_mkdir kprobe hook", &[], // Labels are not needed since `collect` will add them all ); From f276e1f8a409d0e63953d702ed24811a83431ace Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 10:55:52 -0700 Subject: [PATCH 11/20] Fixed clippy errors --- fact/src/bpf/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 14723d53..d4f79260 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -24,9 +24,11 @@ mod checks; const RINGBUFFER_NAME: &str = "rb"; +// Links are stored to keep BPF programs attached - they auto-detach on drop. +// Fields are prefixed with _ to indicate they're kept for Drop behavior, not direct access. enum Link { - Lsm(LsmLink), - KProbe(KProbeLink), + Lsm { _link: LsmLink }, + KProbe { _link: KProbeLink }, } pub struct Bpf { @@ -200,7 +202,7 @@ impl Bpf { match prog { Program::Lsm(prog) => { let link_id = prog.attach()?; - Ok(Link::Lsm(prog.take_link(link_id)?)) + Ok(Link::Lsm { _link: prog.take_link(link_id)? }) } Program::KProbe(prog) => { // Extract function name from program name @@ -215,7 +217,7 @@ impl Bpf { }; let link_id = prog.attach(func_name, 0)?; - Ok(Link::KProbe(prog.take_link(link_id)?)) + Ok(Link::KProbe { _link: prog.take_link(link_id)? }) } u => unimplemented!("{u:?}"), } From e09db7a67ae5c073c8e27fa5a85ea659e3b7557a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 11:30:49 -0700 Subject: [PATCH 12/20] Added comments and fixed format errors --- fact-ebpf/src/bpf/main.c | 11 +++++++++++ fact/src/bpf/mod.rs | 10 +++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 5c8c55d6..676e9a9f 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -230,6 +230,14 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, } // Map to store vfs_mkdir parameters from entry to exit +// Map to store vfs_mkdir parameters from entry to exit +// Key: pid_tgid from bpf_get_current_pid_tgid() to handle concurrent calls +// +// Limitation: This assumes vfs_mkdir doesn't recurse (same thread calling +// vfs_mkdir before a previous call returns). If recursion occurs, nested +// calls would overwrite each other's parameters. In practice, vfs_mkdir at +// the VFS layer rarely recurses, making this acceptable for monitoring +// typical container/host filesystem operations. struct vfs_mkdir_args_t { struct inode* dir; struct dentry* dentry; @@ -243,6 +251,9 @@ struct { } vfs_mkdir_args SEC(".maps"); // Capture parameters at function entry +// We store dir and dentry in a map because they're in registers at entry +// but won't be accessible at exit (kretprobe). The pid_tgid key ensures +// each thread gets its own entry, allowing concurrent mkdir operations. SEC("kprobe/vfs_mkdir") int trace_vfs_mkdir_entry(struct pt_regs* ctx) { u64 pid_tgid = bpf_get_current_pid_tgid(); diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index d4f79260..6b88dce9 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Context, bail}; use aya::{ Btf, Ebpf, maps::{HashMap, LpmTrie, MapData, PerCpuArray, RingBuf}, - programs::{Program, lsm::LsmLink, kprobe::KProbeLink}, + programs::{Program, kprobe::KProbeLink, lsm::LsmLink}, }; use checks::Checks; use globset::{Glob, GlobSet, GlobSetBuilder}; @@ -202,7 +202,9 @@ impl Bpf { match prog { Program::Lsm(prog) => { let link_id = prog.attach()?; - Ok(Link::Lsm { _link: prog.take_link(link_id)? }) + Ok(Link::Lsm { + _link: prog.take_link(link_id)?, + }) } Program::KProbe(prog) => { // Extract function name from program name @@ -217,7 +219,9 @@ impl Bpf { }; let link_id = prog.attach(func_name, 0)?; - Ok(Link::KProbe { _link: prog.take_link(link_id)? }) + Ok(Link::KProbe { + _link: prog.take_link(link_id)?, + }) } u => unimplemented!("{u:?}"), } From 1b84ba09c8fde26ae70b7b30199e4b1a4da03b39 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:09:22 -0700 Subject: [PATCH 13/20] Using is_monitored --- fact-ebpf/src/bpf/main.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 676e9a9f..0b6bca49 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -297,16 +297,6 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { bpf_probe_read_kernel(&child_inode, sizeof(child_inode), &dentry->d_inode); inode_key_t child_key = inode_to_key(child_inode); - // Check if the parent is monitored by looking it up in the inode map - inode_value_t* parent_value = bpf_map_lookup_elem(&inode_map, &parent_key); - if (parent_value == NULL) { - m->path_mkdir.ignored++; - goto cleanup; - } - - // Add the child directory to the inode map - inode_add(&child_key); - // Construct path with just the directory name // Userspace will use the parent inode to construct the full host_path struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); @@ -332,7 +322,21 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { goto cleanup; } - submit_mkdir_event(&m->path_mkdir, bound_path->path, &child_key, &parent_key); + // Check if parent is monitored using the standard is_monitored() function + inode_key_t* child_to_submit = &child_key; + inode_monitored_t status = is_monitored(child_key, bound_path, &parent_key, &child_to_submit); + + if (status == PARENT_MONITORED) { + // Parent is monitored, add the new child directory to tracking + inode_add(&child_key); + } + + if (status == NOT_MONITORED) { + m->path_mkdir.ignored++; + goto cleanup; + } + + submit_mkdir_event(&m->path_mkdir, bound_path->path, child_to_submit, &parent_key); cleanup: bpf_map_delete_elem(&vfs_mkdir_args, &pid_tgid); From 814e3bdeb1a1aeeba45a92e792db084990e0aecc Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:13:52 -0700 Subject: [PATCH 14/20] Using BPF_CORE_READ instead of bpf_probe_read_kernel --- 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 0b6bca49..d32f0e74 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -293,8 +293,7 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { inode_key_t parent_key = inode_to_key(dir); // Get child inode from the created dentry - struct inode* child_inode; - bpf_probe_read_kernel(&child_inode, sizeof(child_inode), &dentry->d_inode); + struct inode* child_inode = BPF_CORE_READ(dentry, d_inode); inode_key_t child_key = inode_to_key(child_inode); // Construct path with just the directory name From 5bbbf68d52287290f4fc8e4ca889922859d57772 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:20:50 -0700 Subject: [PATCH 15/20] Simplified switch (path_append_dentry(bound_path, dentry)) --- fact-ebpf/src/bpf/main.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index d32f0e74..972481a6 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -308,17 +308,9 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { bound_path->path[0] = '/'; bound_path->len = 1; - switch (path_append_dentry(bound_path, dentry)) { - case PATH_APPEND_SUCCESS: - break; - case PATH_APPEND_INVALID_LENGTH: - bpf_printk("Invalid path length: %u", bound_path->len); - m->path_mkdir.error++; - goto cleanup; - case PATH_APPEND_READ_ERROR: - bpf_printk("Failed to read dentry name"); - m->path_mkdir.error++; - goto cleanup; + if (path_append_dentry(bound_path, dentry) != PATH_APPEND_SUCCESS) { + m->path_mkdir.error++; + goto cleanup; } // Check if parent is monitored using the standard is_monitored() function From 60196473254fad06829beaad95511cb292cb5621 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:36:18 -0700 Subject: [PATCH 16/20] Some refactoring of handle_creation_event --- fact/src/host_scanner.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 8780b99e..176ff3f8 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -201,34 +201,29 @@ impl HostScanner { let inode = *event.get_inode(); let parent_inode = *event.get_parent_inode(); - if self.get_host_path(Some(&inode)).is_some() { + if self.get_host_path(Some(&inode)).is_some() || parent_inode.empty() { return Ok(()); } - if parent_inode.empty() { + let Some(filename) = event.get_filename().file_name() else { return Ok(()); - } - - if let Some(filename) = event.get_filename().file_name() { - let filename = filename.to_os_string(); - - if let Some(parent_host_path) = self.get_host_path(Some(&parent_inode)) { - let host_path = parent_host_path.join(&filename); + }; + let filename = filename.to_os_string(); - // Update the event's filename to the full path when it was constructed from parent - event.set_filename(host_path.clone()); + let Some(parent_host_path) = self.get_host_path(Some(&parent_inode)) else { + return Ok(()); + }; - self.update_entry_with_inode(inode, host_path) - .with_context(|| { - format!( - "Failed to add creation event entry for {}", - filename.to_string_lossy() - ) - })?; - } - } + // Construct full path and update tracking + let host_path = parent_host_path.join(&filename); + event.set_filename(host_path.clone()); - Ok(()) + self.update_entry_with_inode(inode, host_path).with_context(|| { + format!( + "Failed to add creation event entry for {}", + filename.to_string_lossy() + ) + }) } /// Periodically notify the host scanner main task that a scan needs From 19be40717c33e960227f47d39192ee37f3d250a3 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:39:45 -0700 Subject: [PATCH 17/20] Removed a couple of comments --- fact-ebpf/src/bpf/main.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 972481a6..f22edb93 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -313,12 +313,10 @@ int trace_vfs_mkdir(struct pt_regs* ctx) { goto cleanup; } - // Check if parent is monitored using the standard is_monitored() function inode_key_t* child_to_submit = &child_key; inode_monitored_t status = is_monitored(child_key, bound_path, &parent_key, &child_to_submit); if (status == PARENT_MONITORED) { - // Parent is monitored, add the new child directory to tracking inode_add(&child_key); } From 8aa8d0666c90dcd25a9ba3c34b77a8c47a33a8e9 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 15:55:34 -0700 Subject: [PATCH 18/20] Fixed format issue --- fact/src/host_scanner.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 176ff3f8..8375211d 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -218,12 +218,13 @@ impl HostScanner { let host_path = parent_host_path.join(&filename); event.set_filename(host_path.clone()); - self.update_entry_with_inode(inode, host_path).with_context(|| { - format!( - "Failed to add creation event entry for {}", - filename.to_string_lossy() - ) - }) + self.update_entry_with_inode(inode, host_path) + .with_context(|| { + format!( + "Failed to add creation event entry for {}", + filename.to_string_lossy() + ) + }) } /// Periodically notify the host scanner main task that a scan needs From 1e04d269d99e73ce1e56e95e3e9c1558dda215aa Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 16:24:06 -0700 Subject: [PATCH 19/20] Removed an unneeded comment --- fact-ebpf/src/bpf/d_path.h | 1 - 1 file changed, 1 deletion(-) diff --git a/fact-ebpf/src/bpf/d_path.h b/fact-ebpf/src/bpf/d_path.h index ba2bce05..6867a0dd 100644 --- a/fact-ebpf/src/bpf/d_path.h +++ b/fact-ebpf/src/bpf/d_path.h @@ -142,7 +142,6 @@ __always_inline static long __d_path(const struct path* path, char* buf, int buf __always_inline static long d_path(const struct path* path, char* buf, int buflen, bool use_bpf_helper) { if (use_bpf_helper) { - // bpf_d_path is a kernel helper that doesn't take const, so we must cast here return bpf_d_path((struct path*)path, buf, buflen); } return __d_path(path, buf, buflen); From 06c46849243fca2d9639fa2ca707f644ee3fdc4c Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 30 Mar 2026 16:32:03 -0700 Subject: [PATCH 20/20] Removed redundant 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])