From 7b92eab719aaff91fb47cd622985ae637dc9e9c7 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 07:39:10 -0700 Subject: [PATCH 01/22] X-Smart-Branch-Parent: main From 0d9cf751a4c7d893b1593933de118f9ed764ad09 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 14 Mar 2026 18:15:11 -0700 Subject: [PATCH 02/22] When a monitored file is created it is added to maps in kernel and user space --- fact-ebpf/src/bpf/inode.h | 8 ++++ fact-ebpf/src/bpf/main.c | 29 +++++++++++++ fact/src/event/mod.rs | 6 ++- fact/src/host_scanner.rs | 28 ++++++++++++- tests/test_inode_tracking.py | 79 ++++++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 tests/test_inode_tracking.py diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index 4e9a26dc..db8703b8 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -65,6 +65,14 @@ __always_inline static inode_value_t* inode_get(struct inode_key_t* inode) { return bpf_map_lookup_elem(&inode_map, inode); } +__always_inline static long inode_add(struct inode_key_t* inode) { + if (inode == NULL) { + return -1; + } + inode_value_t value = 0; + return bpf_map_update_elem(&inode_map, inode, &value, BPF_ANY); +} + __always_inline static long inode_remove(struct inode_key_t* inode) { if (inode == NULL) { return 0; diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index e0a23522..e2a7fd73 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -47,6 +47,35 @@ int BPF_PROG(trace_file_open, struct file* file) { inode_key_t inode_key = inode_to_key(file->f_inode); inode_key_t* inode_to_submit = &inode_key; + // For file creation events, check if the parent directory is being + // monitored. If so, add the new file's inode to the tracked set. + if (event_type == FILE_ACTIVITY_CREATION) { + struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); + if (parent_dentry) { + // Build the parent inode key by reading fields directly + // to avoid verifier issues with untrusted pointers. + // We need to replicate the logic from inode_to_key() to handle + // special filesystems like btrfs correctly. + inode_key_t parent_key = {0}; + parent_key.inode = BPF_CORE_READ(parent_dentry, d_inode, i_ino); + + unsigned long magic = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_magic); + unsigned long parent_dev; + + if (magic == BTRFS_SUPER_MAGIC && bpf_core_type_exists(struct btrfs_inode)) { + parent_dev = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_dev); + } else { + parent_dev = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_dev); + } + + parent_key.dev = new_encode_dev(parent_dev); + + if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { + inode_add(&inode_key); + } + } + } + if (!is_monitored(inode_key, path, &inode_to_submit)) { goto ignored; } diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 9087f5ab..a82ab8fa 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -125,6 +125,10 @@ impl Event { }) } + pub fn is_creation(&self) -> bool { + matches!(self.file, FileData::Creation(_)) + } + /// Unwrap the inner FileData and return the inode that triggered /// the event. /// @@ -151,7 +155,7 @@ impl Event { } } - fn get_filename(&self) -> &PathBuf { + pub fn get_filename(&self) -> &PathBuf { match &self.file { FileData::Open(data) => &data.filename, FileData::Creation(data) => &data.filename, diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 05d22e75..de938946 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -126,7 +126,7 @@ impl HostScanner { for entry in glob::glob(glob_str)? { match entry { Ok(path) => { - if path.is_file() { + if path.is_file() || path.is_dir() { self.metrics.scan_inc(ScanLabels::FileScanned); self.update_entry(path.as_path()).with_context(|| { format!("Failed to update entry for {}", path.display()) @@ -178,6 +178,25 @@ impl HostScanner { self.inode_map.borrow().get(inode?).cloned() } + /// Handle file creation events by adding new inodes to the map. + fn handle_creation_event(&self, event: &Event) -> anyhow::Result<()> { + if self.get_host_path(Some(event.get_inode())).is_some() { + return Ok(()); + } + + let host_path = host_info::prepend_host_mount(event.get_filename()); + + if host_path.exists() { + self.update_entry(&host_path) + .with_context(|| format!("Failed to add creation event entry for {}", host_path.display()))?; + } else { + debug!("Creation event for non-existent file: {}", host_path.display()); + self.metrics.scan_inc(ScanLabels::FileRemoved); + } + + Ok(()) + } + /// Periodically notify the host scanner main task that a scan needs /// to happen. /// @@ -219,6 +238,13 @@ impl HostScanner { }; self.metrics.events.added(); + // Handle file creation events by adding new inodes to the map + if event.is_creation() { + if let Err(e) = self.handle_creation_event(&event) { + warn!("Failed to handle creation event: {e}"); + } + } + if let Some(host_path) = self.get_host_path(Some(event.get_inode())) { self.metrics.scan_inc(ScanLabels::InodeHit); event.set_host_path(host_path); diff --git a/tests/test_inode_tracking.py b/tests/test_inode_tracking.py new file mode 100644 index 00000000..18c41149 --- /dev/null +++ b/tests/test_inode_tracking.py @@ -0,0 +1,79 @@ +""" +Test that verifies inode tracking for newly created files. + +Expected behavior: +1. File created in monitored directory +2. BPF adds inode to kernel map (if parent is monitored) +3. Creation event has non-zero inode +4. Subsequent events on that file should also have the inode populated +""" + +import os +from tempfile import NamedTemporaryFile + +import pytest +import yaml + +from event import Event, EventType, Process + + +@pytest.fixture +def fact_config(monitored_dir, logs_dir): + """ + Config that includes both the directory and its contents. + This ensures the parent directory inode is tracked. + """ + cwd = os.getcwd() + config = { + 'paths': [f'{monitored_dir}/**', '/mounted/**', '/container-dir/**'], + 'grpc': { + 'url': 'http://127.0.0.1:9999', + }, + 'endpoint': { + 'address': '127.0.0.1:9000', + 'expose_metrics': True, + 'health_check': True, + }, + 'json': True, + } + config_file = NamedTemporaryFile( + prefix='fact-config-', suffix='.yml', dir=cwd, mode='w') + yaml.dump(config, config_file) + + yield config, config_file.name + with open(os.path.join(logs_dir, 'fact.yml'), 'w') as f: + with open(config_file.name, 'r') as r: + f.write(r.read()) + config_file.close() + + +def test_inode_tracking_on_creation(monitored_dir, test_file, server): + """ + Test that when a file is created in a monitored directory, + its inode is added to the tracking map. + + The test_file fixture ensures the directory exists and has content + when fact starts, so the parent directory inode gets tracked. + """ + # Create a new file + fut = os.path.join(monitored_dir, 'new_file.txt') + with open(fut, 'w') as f: + f.write('initial content') + + # Wait for creation event + process = Process.from_proc() + creation_event = Event(process=process, event_type=EventType.CREATION, + file=fut, host_path='') + + server.wait_events([creation_event]) + + # Now modify the file - the inode should be tracked from creation + with open(fut, 'a') as f: + f.write('appended content') + + # This open event should have host_path populated because the inode + # was added to the map during creation + open_event = Event(process=process, event_type=EventType.OPEN, + file=fut, host_path=fut) + + server.wait_events([open_event]) From b0113c2f30539d78c44e0a17d9be6fb57bb2822e Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 16 Mar 2026 11:17:13 -0700 Subject: [PATCH 03/22] Using inode_to_key directly instead of duplicating code. inode_to_key can now use untrusted pointers --- fact-ebpf/src/bpf/inode.h | 8 ++++---- fact-ebpf/src/bpf/main.c | 19 ++----------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index db8703b8..a247ec28 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -33,12 +33,12 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { return key; } - unsigned long magic = inode->i_sb->s_magic; + unsigned long magic = BPF_CORE_READ(inode, i_sb, s_magic); switch (magic) { case BTRFS_SUPER_MAGIC: if (bpf_core_type_exists(struct btrfs_inode)) { struct btrfs_inode* btrfs_inode = container_of(inode, struct btrfs_inode, vfs_inode); - key.inode = inode->i_ino; + key.inode = BPF_CORE_READ(inode, i_ino); key.dev = BPF_CORE_READ(btrfs_inode, root, anon_dev); break; } @@ -46,8 +46,8 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { // supported on the system. Fallback to the generic implementation // just in case. default: - key.inode = inode->i_ino; - key.dev = inode->i_sb->s_dev; + key.inode = BPF_CORE_READ(inode, i_ino); + key.dev = BPF_CORE_READ(inode, i_sb, s_dev); break; } diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index e2a7fd73..701d964b 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -52,23 +52,8 @@ int BPF_PROG(trace_file_open, struct file* file) { if (event_type == FILE_ACTIVITY_CREATION) { struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); if (parent_dentry) { - // Build the parent inode key by reading fields directly - // to avoid verifier issues with untrusted pointers. - // We need to replicate the logic from inode_to_key() to handle - // special filesystems like btrfs correctly. - inode_key_t parent_key = {0}; - parent_key.inode = BPF_CORE_READ(parent_dentry, d_inode, i_ino); - - unsigned long magic = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_magic); - unsigned long parent_dev; - - if (magic == BTRFS_SUPER_MAGIC && bpf_core_type_exists(struct btrfs_inode)) { - parent_dev = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_dev); - } else { - parent_dev = BPF_CORE_READ(parent_dentry, d_inode, i_sb, s_dev); - } - - parent_key.dev = new_encode_dev(parent_dev); + struct inode* parent_inode = BPF_CORE_READ(parent_dentry, d_inode); + inode_key_t parent_key = inode_to_key(parent_inode); if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { inode_add(&inode_key); From 2bf0f3db93d88b3b91621871324e2819ba9a1bdd Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Tue, 17 Mar 2026 19:49:21 -0700 Subject: [PATCH 04/22] Parent inode is added to events. That is used to get the correct path --- fact-ebpf/src/bpf/events.h | 23 +++++++++++++------- fact-ebpf/src/bpf/main.c | 43 +++++++++++++++++++++++++++---------- fact-ebpf/src/bpf/types.h | 1 + fact/src/event/mod.rs | 23 +++++++++++++++++--- fact/src/host_scanner.rs | 44 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 110 insertions(+), 24 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index abe4d19c..26254778 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -17,10 +17,12 @@ __always_inline static void __submit_event(struct event_t* event, file_activity_type_t event_type, const char filename[PATH_MAX], inode_key_t* inode, + inode_key_t* parent_inode, bool use_bpf_d_path) { event->type = event_type; event->timestamp = bpf_ktime_get_boot_ns(); inode_copy_or_reset(&event->inode, inode); + inode_copy_or_reset(&event->parent_inode, parent_inode); bpf_probe_read_str(event->filename, PATH_MAX, filename); struct helper_t* helper = get_helper(); @@ -46,31 +48,34 @@ __always_inline static void __submit_event(struct event_t* event, __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* inode, + inode_key_t* parent_inode) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); if (event == NULL) { m->ringbuffer_full++; return; } - __submit_event(event, m, event_type, filename, inode, true); + __submit_event(event, m, event_type, filename, inode, parent_inode, true); } __always_inline static void submit_unlink_event(struct metrics_by_hook_t* m, const char filename[PATH_MAX], - inode_key_t* inode) { + inode_key_t* inode, + inode_key_t* parent_inode) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); if (event == NULL) { m->ringbuffer_full++; return; } - __submit_event(event, m, FILE_ACTIVITY_UNLINK, filename, inode, path_hooks_support_bpf_d_path); + __submit_event(event, m, FILE_ACTIVITY_UNLINK, filename, inode, parent_inode, path_hooks_support_bpf_d_path); } __always_inline static void submit_mode_event(struct metrics_by_hook_t* m, const char filename[PATH_MAX], inode_key_t* inode, + inode_key_t* parent_inode, umode_t mode, umode_t old_mode) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); @@ -82,12 +87,13 @@ __always_inline static void submit_mode_event(struct metrics_by_hook_t* m, event->chmod.new = mode; event->chmod.old = old_mode; - __submit_event(event, m, FILE_ACTIVITY_CHMOD, filename, inode, path_hooks_support_bpf_d_path); + __submit_event(event, m, FILE_ACTIVITY_CHMOD, filename, inode, parent_inode, path_hooks_support_bpf_d_path); } __always_inline static void submit_ownership_event(struct metrics_by_hook_t* m, const char filename[PATH_MAX], inode_key_t* inode, + inode_key_t* parent_inode, unsigned long long uid, unsigned long long gid, unsigned long long old_uid, @@ -103,14 +109,15 @@ __always_inline static void submit_ownership_event(struct metrics_by_hook_t* m, event->chown.old.uid = old_uid; event->chown.old.gid = old_gid; - __submit_event(event, m, FILE_ACTIVITY_CHOWN, filename, inode, path_hooks_support_bpf_d_path); + __submit_event(event, m, FILE_ACTIVITY_CHOWN, filename, inode, parent_inode, path_hooks_support_bpf_d_path); } __always_inline static void submit_rename_event(struct metrics_by_hook_t* m, const char new_filename[PATH_MAX], const char old_filename[PATH_MAX], inode_key_t* new_inode, - inode_key_t* old_inode) { + inode_key_t* old_inode, + inode_key_t* new_parent_inode) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); if (event == NULL) { m->ringbuffer_full++; @@ -120,5 +127,5 @@ __always_inline static void submit_rename_event(struct metrics_by_hook_t* m, bpf_probe_read_str(event->rename.old_filename, PATH_MAX, old_filename); inode_copy_or_reset(&event->rename.old_inode, old_inode); - __submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, path_hooks_support_bpf_d_path); + __submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, new_parent_inode, path_hooks_support_bpf_d_path); } diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 701d964b..84033a2a 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -47,17 +47,16 @@ int BPF_PROG(trace_file_open, struct file* file) { inode_key_t inode_key = inode_to_key(file->f_inode); inode_key_t* inode_to_submit = &inode_key; + // Extract parent inode + struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); + struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; + inode_key_t parent_key = inode_to_key(parent_inode_ptr); + // For file creation events, check if the parent directory is being // monitored. If so, add the new file's inode to the tracked set. if (event_type == FILE_ACTIVITY_CREATION) { - struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); - if (parent_dentry) { - struct inode* parent_inode = BPF_CORE_READ(parent_dentry, d_inode); - inode_key_t parent_key = inode_to_key(parent_inode); - - if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { - inode_add(&inode_key); - } + if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { + inode_add(&inode_key); } } @@ -65,7 +64,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); + submit_open_event(&m->file_open, event_type, path->path, inode_to_submit, &parent_key); return 0; @@ -93,6 +92,10 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { inode_key_t inode_key = inode_to_key(dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; + // Extract parent inode from dir parameter + struct inode* parent_inode = BPF_CORE_READ(dir, dentry, d_inode); + inode_key_t parent_key = inode_to_key(parent_inode); + if (!is_monitored(inode_key, path, &inode_to_submit)) { m->path_unlink.ignored++; return 0; @@ -100,7 +103,8 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { submit_unlink_event(&m->path_unlink, path->path, - inode_to_submit); + inode_to_submit, + &parent_key); return 0; } @@ -123,6 +127,11 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; + // Extract parent inode + struct dentry* parent_dentry = BPF_CORE_READ(path, dentry, d_parent); + struct inode* parent_inode = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; + inode_key_t parent_key = inode_to_key(parent_inode); + if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { m->path_chmod.ignored++; return 0; @@ -132,6 +141,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { submit_mode_event(&m->path_chmod, bound_path->path, inode_to_submit, + &parent_key, mode, old_mode); @@ -160,6 +170,11 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; + // Extract parent inode + struct dentry* parent_dentry = BPF_CORE_READ(path, dentry, d_parent); + struct inode* parent_inode = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; + inode_key_t parent_key = inode_to_key(parent_inode); + if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { m->path_chown.ignored++; return 0; @@ -172,6 +187,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign submit_ownership_event(&m->path_chown, bound_path->path, inode_to_submit, + &parent_key, uid, gid, old_uid, @@ -209,6 +225,10 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, inode_key_t* old_inode_submit = &old_inode; inode_key_t* new_inode_submit = &new_inode; + // Extract new parent inode from new_dir + struct inode* new_parent_inode = BPF_CORE_READ(new_dir, dentry, d_inode); + inode_key_t new_parent_key = inode_to_key(new_parent_inode); + bool old_monitored = is_monitored(old_inode, old_path, &old_inode_submit); bool new_monitored = is_monitored(new_inode, new_path, &new_inode_submit); @@ -221,7 +241,8 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, new_path->path, old_path->path, old_inode_submit, - new_inode_submit); + new_inode_submit, + &new_parent_key); return 0; error: diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 6a009e66..55005c00 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -62,6 +62,7 @@ struct event_t { process_t process; char filename[PATH_MAX]; inode_key_t inode; + inode_key_t parent_inode; file_activity_type_t type; union { struct { diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index a82ab8fa..7cc422c5 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -93,6 +93,7 @@ impl Event { filename, host_file, inode: Default::default(), + parent_inode: Default::default(), }; let file = match data { EventTestData::Creation => FileData::Creation(inner), @@ -145,6 +146,18 @@ impl Event { } } + /// Get the parent inode for the file in this event. + pub fn get_parent_inode(&self) -> &inode_key_t { + match &self.file { + FileData::Open(data) => &data.parent_inode, + FileData::Creation(data) => &data.parent_inode, + FileData::Unlink(data) => &data.parent_inode, + FileData::Chmod(data) => &data.inner.parent_inode, + FileData::Chown(data) => &data.inner.parent_inode, + FileData::Rename(data) => &data.new.parent_inode, + } + } + /// Same as `get_inode` but returning the 'old' inode for operations /// like rename. For operations that involve a single inode, `None` /// will be returned. @@ -237,6 +250,7 @@ impl TryFrom<&event_t> for Event { value.type_, value.filename, value.inode, + value.parent_inode, value.__bindgen_anon_1, )?; @@ -286,9 +300,10 @@ impl FileData { event_type: file_activity_type_t, filename: [c_char; PATH_MAX as usize], inode: inode_key_t, + parent_inode: inode_key_t, extra_data: fact_ebpf::event_t__bindgen_ty_1, ) -> anyhow::Result { - let inner = BaseFileData::new(filename, inode)?; + 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), @@ -316,7 +331,7 @@ impl FileData { let old_inode = unsafe { extra_data.rename.old_inode }; let data = RenameFileData { new: inner, - old: BaseFileData::new(old_filename, old_inode)?, + old: BaseFileData::new(old_filename, old_inode, Default::default())?, }; FileData::Rename(data) } @@ -380,14 +395,16 @@ pub struct BaseFileData { pub filename: PathBuf, host_file: PathBuf, inode: inode_key_t, + parent_inode: inode_key_t, } impl BaseFileData { - pub fn new(filename: [c_char; PATH_MAX as usize], inode: inode_key_t) -> anyhow::Result { + pub fn new(filename: [c_char; PATH_MAX as usize], inode: inode_key_t, parent_inode: inode_key_t) -> anyhow::Result { Ok(BaseFileData { filename: sanitize_d_path(&filename), host_file: PathBuf::new(), // this field is set by HostScanner inode, + parent_inode, }) } } diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index de938946..63dacf83 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -126,11 +126,16 @@ impl HostScanner { for entry in glob::glob(glob_str)? { match entry { Ok(path) => { - if path.is_file() || path.is_dir() { + if path.is_file() { self.metrics.scan_inc(ScanLabels::FileScanned); self.update_entry(path.as_path()).with_context(|| { format!("Failed to update entry for {}", path.display()) })?; + } else if path.is_dir() { + self.metrics.scan_inc(ScanLabels::DirectoryScanned); + self.update_entry(path.as_path()).with_context(|| { + format!("Failed to update entry for {}", path.display()) + })?; } else { self.metrics.scan_inc(ScanLabels::FsItemIgnored); } @@ -179,16 +184,51 @@ impl HostScanner { } /// Handle file creation events by adding new inodes to the map. + /// + /// For creation events, 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<()> { if self.get_host_path(Some(event.get_inode())).is_some() { return Ok(()); } - let host_path = host_info::prepend_host_mount(event.get_filename()); + let parent_inode = event.get_parent_inode(); + + if parent_inode.empty() { + debug!("Creation event has no parent inode: {}", event.get_filename().display()); + return Ok(()); + } + + let event_filename = event.get_filename(); + let Some(filename) = event_filename.file_name() else { + debug!("Creation event has no filename component: {}", event_filename.display()); + return Ok(()); + }; + + let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) else { + debug!("Parent inode not in map, using prepend_host_mount for: {}", event_filename.display()); + let host_path = host_info::prepend_host_mount(event_filename); + if host_path.exists() { + return self.update_entry(&host_path) + .with_context(|| format!("Failed to add creation event entry for {}", host_path.display())); + } + return Ok(()); + }; + + let host_path = parent_host_path.join(filename); + + debug!( + "Constructed host path for creation event: {} (from container path: {}, parent host path: {})", + host_path.display(), + event_filename.display(), + parent_host_path.display() + ); if host_path.exists() { self.update_entry(&host_path) .with_context(|| format!("Failed to add creation event entry for {}", host_path.display()))?; + debug!("Successfully added inode entry for newly created file: {}", host_path.display()); } else { debug!("Creation event for non-existent file: {}", host_path.display()); self.metrics.scan_inc(ScanLabels::FileRemoved); From 1b64a72d2538cf80442190d4494cd451df4c6ac5 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 11:22:15 -0700 Subject: [PATCH 05/22] Using the inode directly to add to map instead of using the path to get the inode and then add to the map --- fact/src/host_scanner.rs | 43 +++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 63dacf83..f1f43507 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -173,6 +173,25 @@ impl HostScanner { Ok(()) } + // Similar to update_entry except we are are directly using the inode instead of the path. + fn update_entry_with_inode(&self, inode: &inode_key_t, path: PathBuf) -> anyhow::Result<()> { + debug!("Adding entry for {}: {inode:?}", path.display()); + + self.kernel_inode_map + .borrow_mut() + .insert(*inode, 0, 0) + .with_context(|| format!("Failed to insert kernel entry for {}", path.display()))?; + let mut inode_map = self.inode_map.borrow_mut(); + let entry = inode_map.entry(*inode).or_default(); + // Not removing the host mount, which is done in update_entry. + // I am not sure if that is correct. + *entry = path; + + self.metrics.scan_inc(ScanLabels::FileUpdated); + + Ok(()) + } + pub fn subscribe(&self) -> broadcast::Receiver> { self.tx.subscribe() } @@ -185,11 +204,13 @@ impl HostScanner { /// Handle file creation events by adding new inodes to the map. /// - /// For creation events, we use the parent inode provided by the eBPF code + /// 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<()> { - if self.get_host_path(Some(event.get_inode())).is_some() { + let inode = event.get_inode(); + + if self.get_host_path(Some(inode)).is_some() { return Ok(()); } @@ -207,12 +228,7 @@ impl HostScanner { }; let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) else { - debug!("Parent inode not in map, using prepend_host_mount for: {}", event_filename.display()); - let host_path = host_info::prepend_host_mount(event_filename); - if host_path.exists() { - return self.update_entry(&host_path) - .with_context(|| format!("Failed to add creation event entry for {}", host_path.display())); - } + debug!("Parent inode not in map, cannot construct host path for: {}", event_filename.display()); return Ok(()); }; @@ -225,14 +241,9 @@ impl HostScanner { parent_host_path.display() ); - if host_path.exists() { - self.update_entry(&host_path) - .with_context(|| format!("Failed to add creation event entry for {}", host_path.display()))?; - debug!("Successfully added inode entry for newly created file: {}", host_path.display()); - } else { - debug!("Creation event for non-existent file: {}", host_path.display()); - self.metrics.scan_inc(ScanLabels::FileRemoved); - } + self.update_entry_with_inode(inode, host_path) + .with_context(|| format!("Failed to add creation event entry for {}", event_filename.display()))?; + debug!("Successfully added inode entry for newly created file: {}", event_filename.display()); Ok(()) } From 35462c41a5992b31c1b689ec8e67d896d33e9d72 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 11:40:31 -0700 Subject: [PATCH 06/22] Added host path to tests --- tests/conftest.py | 10 ++++++++- tests/test_config_hotreload.py | 10 ++++----- tests/test_editors/test_nvim.py | 21 +++++++++++-------- tests/test_editors/test_sed.py | 12 ++++++----- tests/test_editors/test_vi.py | 37 +++++++++++++++++++-------------- tests/test_editors/test_vim.py | 37 +++++++++++++++++++-------------- tests/test_file_open.py | 9 ++++---- tests/test_inode_tracking.py | 4 ++-- tests/test_path_chmod.py | 13 ++++++------ tests/test_path_rename.py | 23 +++++++++++++++++--- tests/test_path_unlink.py | 13 ++++++------ tests/test_wildcard.py | 14 ++++++------- 12 files changed, 123 insertions(+), 80 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4267333e..cfb6be05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,7 +104,15 @@ def dump_logs(container, file): def fact_config(request, monitored_dir, logs_dir): cwd = os.getcwd() config = { - 'paths': [f'{monitored_dir}/**/*', '/mounted/**/*', '/container-dir/**/*'], + 'paths': [ + f'{monitored_dir}', + f'{monitored_dir}/*', + f'{monitored_dir}/**', + '/mounted', + '/mounted/**', + '/container-dir', + '/container-dir/**', + ], 'grpc': { 'url': 'http://127.0.0.1:9999', }, diff --git a/tests/test_config_hotreload.py b/tests/test_config_hotreload.py index 3dfa445e..d276582f 100644 --- a/tests/test_config_hotreload.py +++ b/tests/test_config_hotreload.py @@ -99,7 +99,7 @@ def test_output_grpc_address_change(fact, fact_config, monitored_dir, server, al process = Process.from_proc() e = Event(process=process, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) @@ -112,7 +112,7 @@ def test_output_grpc_address_change(fact, fact_config, monitored_dir, server, al f.write('This is another test') e = Event(process=process, event_type=EventType.OPEN, - file=fut, host_path='') + file=fut, host_path=fut) alternate_server.wait_events([e]) @@ -131,7 +131,7 @@ def test_paths(fact, fact_config, monitored_dir, ignored_dir, server): f.write('This is a test') e = Event(process=p, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) @@ -202,7 +202,7 @@ def test_paths_then_remove(fact, fact_config, monitored_dir, server): f.write('This is a test') e = Event(process=p, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) @@ -234,7 +234,7 @@ def test_paths_addition(fact, fact_config, monitored_dir, ignored_dir, server): f.write('This is a test') e = Event(process=p, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 9f3d095d..4afcf7dc 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -23,9 +23,11 @@ def test_new_file(editor_container, server): server.wait_events(events, strict=True) -def test_open_file(editor_container, server): +def test_open_file(editor_container, server, ignored_dir): fut = '/mounted/test.txt' + fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' + fut_backup_host = f'{ignored_dir}/test.txt~' cmd = f"nvim {fut} '+:normal iThis is a test' -c x" container_id = editor_container.id[:12] @@ -47,24 +49,25 @@ def test_open_file(editor_container, server): ) vi_test_file = get_vi_test_file('/mounted') + vi_test_file_host = get_vi_test_file(ignored_dir) events = [ Event(process=touch, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=nvim, event_type=EventType.CREATION, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=nvim, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), + file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), Event(process=nvim, event_type=EventType.UNLINK, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=nvim, event_type=EventType.RENAME, - file=fut_backup, host_path='', old_file=fut, old_host_path=''), + file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), Event(process=nvim, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=nvim, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=0o100644), + file=fut, host_path=fut_host, mode=0o100644), Event(process=nvim, event_type=EventType.UNLINK, - file=fut_backup, host_path=''), + file=fut_backup, host_path=fut_backup_host), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index c3f6a2f8..579e6d13 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -2,9 +2,10 @@ from event import Event, EventType, Process -def test_sed(vi_container, server): +def test_sed(vi_container, server, ignored_dir): # File Under Test fut = '/mounted/test.txt' + fut_host = f'{ignored_dir}/test.txt' create_cmd = f"sh -c \"echo 'This is a test' > {fut}\"" sed_cmd = fr'sed -i -e "s/a test/not \\0/" {fut}' container_id = vi_container.id[:12] @@ -26,16 +27,17 @@ def test_sed(vi_container, server): ) sed_tmp_file = re.compile(r'\/mounted\/sed[0-9a-zA-Z]{6}') + sed_tmp_host = re.compile(rf'{re.escape(ignored_dir)}\/sed[0-9a-zA-Z]{{6}}') events = [ Event(process=shell, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=sed, event_type=EventType.CREATION, - file=sed_tmp_file, host_path=''), + file=sed_tmp_file, host_path=sed_tmp_host), Event(process=sed, event_type=EventType.OWNERSHIP, - file=sed_tmp_file, host_path='', owner_uid=0, owner_gid=0), + file=sed_tmp_file, host_path=sed_tmp_host, owner_uid=0, owner_gid=0), Event(process=sed, event_type=EventType.RENAME, - file=fut, host_path='', old_file=sed_tmp_file, old_host_path=''), + file=fut, host_path=fut_host, old_file=sed_tmp_file, old_host_path=sed_tmp_host), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index 4301890a..c576dfd5 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -84,12 +84,17 @@ def test_new_file_ovfs(vi_container, server): server.wait_events(events, strict=True) -def test_open_file(vi_container, server): +def test_open_file(vi_container, server, ignored_dir): fut = '/mounted/test.txt' + fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' + fut_backup_host = f'{ignored_dir}/test.txt~' swap_file = '/mounted/.test.txt.swp' + swap_file_host = f'{ignored_dir}/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' + swx_file_host = f'{ignored_dir}/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') + vi_test_file_host = get_vi_test_file(ignored_dir) exe = '/usr/bin/vi' container_id = vi_container.id[:12] @@ -114,35 +119,35 @@ def test_open_file(vi_container, server): events = [ Event(process=touch_process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swx_file, host_path=''), + file=swx_file, host_path=swx_file_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swx_file, host_path=''), + file=swx_file, host_path=swx_file_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.PERMISSION, - file=swap_file, host_path='', mode=0o644), + file=swap_file, host_path=swap_file_host, mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=vi_process, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), + file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=vi_process, event_type=EventType.RENAME, - file=fut_backup, host_path='', old_file=fut, old_host_path=''), + file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), Event(process=vi_process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=vi_process, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=0o100644), + file=fut, host_path=fut_host, mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=fut_backup, host_path=''), + file=fut_backup, host_path=fut_backup_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 56cbb667..56ecb5c2 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -80,12 +80,17 @@ def test_new_file_ovfs(editor_container, server): server.wait_events(events, strict=True) -def test_open_file(editor_container, server): +def test_open_file(editor_container, server, ignored_dir): fut = '/mounted/test.txt' + fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' + fut_backup_host = f'{ignored_dir}/test.txt~' swap_file = '/mounted/.test.txt.swp' + swap_file_host = f'{ignored_dir}/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' + swx_file_host = f'{ignored_dir}/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') + vi_test_file_host = get_vi_test_file(ignored_dir) container_id = editor_container.id[:12] cmd = f"vim {fut} '+:normal iThis is a test' -c x" @@ -109,35 +114,35 @@ def test_open_file(editor_container, server): events = [ Event(process=touch_process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swx_file, host_path=''), + file=swx_file, host_path=swx_file_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swx_file, host_path=''), + file=swx_file, host_path=swx_file_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), Event(process=vi_process, event_type=EventType.PERMISSION, - file=swap_file, host_path='', mode=0o644), + file=swap_file, host_path=swap_file_host, mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=vi_process, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), + file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, - file=vi_test_file, host_path=''), + file=vi_test_file, host_path=vi_test_file_host), Event(process=vi_process, event_type=EventType.RENAME, - file=fut_backup, host_path='', old_file=fut, old_host_path=''), + file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), Event(process=vi_process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut_host), Event(process=vi_process, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=0o100644), + file=fut, host_path=fut_host, mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=fut_backup, host_path=''), + file=fut_backup, host_path=fut_backup_host), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=''), + file=swap_file, host_path=swap_file_host), ] server.wait_events(events, strict=True) diff --git a/tests/test_file_open.py b/tests/test_file_open.py index aa1ff508..c43b8a15 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -35,7 +35,7 @@ def test_open(monitored_dir, server, filename): fut = path_to_string(fut) e = Event(process=Process.from_proc(), event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) @@ -59,7 +59,7 @@ def test_multiple(monitored_dir, server): f.write('This is a test') events.append( - Event(process=process, event_type=EventType.CREATION, file=fut, host_path='')) + Event(process=process, event_type=EventType.CREATION, file=fut, host_path=fut)) server.wait_events(events) @@ -139,9 +139,9 @@ def test_external_process(monitored_dir, server): p = Process.from_proc(proc.pid) creation = Event(process=p, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) write_access = Event( - process=p, event_type=EventType.OPEN, file=fut, host_path='') + process=p, event_type=EventType.OPEN, file=fut, host_path=fut) try: server.wait_events([creation, write_access]) @@ -186,6 +186,7 @@ def test_mounted_dir(test_container, ignored_dir, server): name='touch', container_id=test_container.id[:12], ) + # ignored_dir is not monitored, so host_path should be blank event = Event(process=process, event_type=EventType.CREATION, file=fut, host_path='') diff --git a/tests/test_inode_tracking.py b/tests/test_inode_tracking.py index 18c41149..2fa83440 100644 --- a/tests/test_inode_tracking.py +++ b/tests/test_inode_tracking.py @@ -25,7 +25,7 @@ def fact_config(monitored_dir, logs_dir): """ cwd = os.getcwd() config = { - 'paths': [f'{monitored_dir}/**', '/mounted/**', '/container-dir/**'], + 'paths': [f'{monitored_dir}', f'{monitored_dir}/*', f'{monitored_dir}/**', '/mounted/**', '/container-dir/**'], 'grpc': { 'url': 'http://127.0.0.1:9999', }, @@ -63,7 +63,7 @@ def test_inode_tracking_on_creation(monitored_dir, test_file, server): # Wait for creation event process = Process.from_proc() creation_event = Event(process=process, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([creation_event]) diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index fde7589f..08cb483d 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -41,9 +41,9 @@ def test_chmod(monitored_dir, server, filename): # We expect both CREATION (from file creation) and PERMISSION (from chmod) events = [ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut), Event(process=process, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=mode), + file=fut, host_path=fut, mode=mode), ] server.wait_events(events) @@ -69,9 +69,9 @@ def test_multiple(monitored_dir, server): events.extend([ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut), Event(process=process, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=mode), + file=fut, host_path=fut, mode=mode), ]) server.wait_events(events) @@ -132,9 +132,9 @@ def test_external_process(monitored_dir, server): events = [ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path='', mode=mode), + file=fut, host_path=fut, mode=mode), Event(process=process, event_type=EventType.PERMISSION, - file=fut, host_path='', mode=mode), + file=fut, host_path=fut, mode=mode), ] try: @@ -213,6 +213,7 @@ def test_mounted_dir(test_container, ignored_dir, server): name='chmod', container_id=test_container.id[:12], ) + # ignored_dir is not monitored, so host_path should be blank events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_path_rename.py b/tests/test_path_rename.py index 1d4f08b7..a3600eff 100644 --- a/tests/test_path_rename.py +++ b/tests/test_path_rename.py @@ -37,13 +37,20 @@ def test_rename(monitored_dir, server, filename): # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD fut = path_to_string(fut) + # TODO: Current behavior is incorrect. The inode map should be updated + # during rename events so that host_path reflects the new path. + # Expected correct behavior: + # - First rename: host_path should be `fut` (new path), old_host_path should be `old_fut` + # - Second rename: host_path should be `old_fut`, old_host_path should be `fut` + # Current behavior: host_path remains the original path (old_fut) because + # the inode map is not updated on rename events. old_host_path is empty. events = [ Event(process=Process.from_proc(), event_type=EventType.CREATION, - file=old_fut, host_path=''), + file=old_fut, host_path=old_fut), Event(process=Process.from_proc(), event_type=EventType.RENAME, - file=fut, host_path='', old_file=old_fut, old_host_path=''), + file=fut, host_path=old_fut, old_file=old_fut, old_host_path=''), Event(process=Process.from_proc(), event_type=EventType.RENAME, - file=old_fut, host_path='', old_file=fut, old_host_path=''), + file=old_fut, host_path=old_fut, old_file=fut, old_host_path=''), ] server.wait_events(events) @@ -76,6 +83,10 @@ def test_ignored(monitored_dir, ignored_dir, server): os.rename(new_path, ignored_path) p = Process.from_proc() + # TODO: Current behavior is incorrect for rename events. + # Expected: When renaming from ignored to monitored, host_path should be new_path. + # When renaming from monitored to ignored, old_host_path should be new_path. + # Current: The inode map is not updated on renames, and old_host_path is not populated. events = [ Event(process=p, event_type=EventType.RENAME, file=new_path, host_path='', old_file=new_ignored_path, old_host_path=''), @@ -122,6 +133,11 @@ def test_rename_dir(monitored_dir, ignored_dir, server): os.rename(new_dut, ignored_dut) p = Process.from_proc() + # TODO: Current behavior is incorrect for rename events. + # Expected: host_path should reflect the new path after rename, + # old_host_path should reflect the old path if it was monitored. + # Current: The inode map is not updated on renames, so host_path remains empty + # or shows the wrong path. old_host_path is not populated. events = [ Event(process=p, event_type=EventType.RENAME, file=dut, host_path='', old_file=new_ignored_dut, old_host_path=''), @@ -188,6 +204,7 @@ def test_mounted_dir(test_container, ignored_dir, server): name='mv', container_id=test_container.id[:12], ) + # ignored_dir is not monitored, so host_path should be blank events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 4dff11da..66a533e6 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -44,9 +44,9 @@ def test_remove(monitored_dir, server, filename): # We expect both CREATION (from file creation) and UNLINK (from removal) events = [ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut), Event(process=process, event_type=EventType.UNLINK, - file=fut, host_path=''), + file=fut, host_path=fut), ] server.wait_events(events) @@ -73,9 +73,9 @@ def test_multiple(monitored_dir, server): events.extend([ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut), Event(process=process, event_type=EventType.UNLINK, - file=fut, host_path=''), + file=fut, host_path=fut), ]) server.wait_events(events) @@ -135,9 +135,9 @@ def test_external_process(monitored_dir, server): events = [ Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=''), + file=fut, host_path=fut), Event(process=process, event_type=EventType.UNLINK, - file=fut, host_path=''), + file=fut, host_path=fut), ] try: @@ -199,6 +199,7 @@ def test_mounted_dir(test_container, ignored_dir, server): name='rm', container_id=test_container.id[:12], ) + # ignored_dir is not monitored, so host_path should be blank events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_wildcard.py b/tests/test_wildcard.py index fd1728f3..aa33c0e1 100644 --- a/tests/test_wildcard.py +++ b/tests/test_wildcard.py @@ -37,7 +37,7 @@ def test_extension_wildcard(wildcard_config, monitored_dir, server): f.write('This should be captured') e = Event(process=process, event_type=EventType.CREATION, - file=txt_file, host_path='') + file=txt_file, host_path=txt_file) server.wait_events([e]) @@ -55,7 +55,7 @@ def test_prefix_wildcard(wildcard_config, monitored_dir, server): f.write('This should be captured') e = Event(process=process, event_type=EventType.CREATION, - file=test_log, host_path='') + file=test_log, host_path=test_log) server.wait_events([e]) @@ -81,9 +81,9 @@ def test_recursive_wildcard(wildcard_config, monitored_dir, server): events = [ Event(process=process, event_type=EventType.CREATION, - file=root_txt, host_path=''), + file=root_txt, host_path=root_txt), Event(process=process, event_type=EventType.CREATION, - file=nested_txt, host_path=''), + file=nested_txt, host_path=nested_txt), ] server.wait_events(events) @@ -97,7 +97,7 @@ def test_nonrecursive_wildcard(wildcard_config, monitored_dir, server): f.write('This should be captured') e = Event(process=process, event_type=EventType.CREATION, - file=fut, host_path='') + file=fut, host_path=fut) server.wait_events([e]) @@ -120,9 +120,9 @@ def test_multiple_patterns(wildcard_config, monitored_dir, server): events = [ Event(process=process, event_type=EventType.CREATION, - file=txt_file, host_path=''), + file=txt_file, host_path=txt_file), Event(process=process, event_type=EventType.CREATION, - file=log_file, host_path=''), + file=log_file, host_path=log_file), ] server.wait_events(events) From 411c115feb6b28d2cca2c1369526ca0710707621 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 11:51:00 -0700 Subject: [PATCH 07/22] Only the open file event returns the parent inode, the rest return null --- fact-ebpf/src/bpf/main.c | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 84033a2a..cc877c68 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -92,10 +92,6 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { inode_key_t inode_key = inode_to_key(dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - // Extract parent inode from dir parameter - struct inode* parent_inode = BPF_CORE_READ(dir, dentry, d_inode); - inode_key_t parent_key = inode_to_key(parent_inode); - if (!is_monitored(inode_key, path, &inode_to_submit)) { m->path_unlink.ignored++; return 0; @@ -104,7 +100,7 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { submit_unlink_event(&m->path_unlink, path->path, inode_to_submit, - &parent_key); + NULL); return 0; } @@ -127,11 +123,6 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - // Extract parent inode - struct dentry* parent_dentry = BPF_CORE_READ(path, dentry, d_parent); - struct inode* parent_inode = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; - inode_key_t parent_key = inode_to_key(parent_inode); - if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { m->path_chmod.ignored++; return 0; @@ -141,7 +132,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { submit_mode_event(&m->path_chmod, bound_path->path, inode_to_submit, - &parent_key, + NULL, mode, old_mode); @@ -170,11 +161,6 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - // Extract parent inode - struct dentry* parent_dentry = BPF_CORE_READ(path, dentry, d_parent); - struct inode* parent_inode = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; - inode_key_t parent_key = inode_to_key(parent_inode); - if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { m->path_chown.ignored++; return 0; @@ -187,7 +173,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign submit_ownership_event(&m->path_chown, bound_path->path, inode_to_submit, - &parent_key, + NULL, uid, gid, old_uid, @@ -225,10 +211,6 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, inode_key_t* old_inode_submit = &old_inode; inode_key_t* new_inode_submit = &new_inode; - // Extract new parent inode from new_dir - struct inode* new_parent_inode = BPF_CORE_READ(new_dir, dentry, d_inode); - inode_key_t new_parent_key = inode_to_key(new_parent_inode); - bool old_monitored = is_monitored(old_inode, old_path, &old_inode_submit); bool new_monitored = is_monitored(new_inode, new_path, &new_inode_submit); @@ -242,7 +224,7 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, old_path->path, old_inode_submit, new_inode_submit, - &new_parent_key); + NULL); return 0; error: From 3b09253f8b218b76bdda4cf48744efab887d196b Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 11:58:35 -0700 Subject: [PATCH 08/22] Removed blank space --- 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 f1f43507..39c1ae24 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -173,7 +173,7 @@ impl HostScanner { Ok(()) } - // Similar to update_entry except we are are directly using the inode instead of the path. + // Similar to update_entry except we are are directly using the inode instead of the path. fn update_entry_with_inode(&self, inode: &inode_key_t, path: PathBuf) -> anyhow::Result<()> { debug!("Adding entry for {}: {inode:?}", path.display()); From f37b40bdedeac5b00a4c98e37f1653b953fc0e75 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 14:35:03 -0700 Subject: [PATCH 09/22] Fixed formatting errors --- fact/src/event/mod.rs | 6 +++++- fact/src/host_scanner.rs | 27 ++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 7cc422c5..40bd317a 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -399,7 +399,11 @@ pub struct BaseFileData { } impl BaseFileData { - pub fn new(filename: [c_char; PATH_MAX as usize], inode: inode_key_t, parent_inode: inode_key_t) -> anyhow::Result { + pub fn new( + filename: [c_char; PATH_MAX as usize], + inode: inode_key_t, + parent_inode: inode_key_t, + ) -> anyhow::Result { Ok(BaseFileData { filename: sanitize_d_path(&filename), host_file: PathBuf::new(), // this field is set by HostScanner diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 39c1ae24..d94824b5 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -217,18 +217,27 @@ impl HostScanner { let parent_inode = event.get_parent_inode(); if parent_inode.empty() { - debug!("Creation event has no parent inode: {}", event.get_filename().display()); + debug!( + "Creation event has no parent inode: {}", + event.get_filename().display() + ); return Ok(()); } let event_filename = event.get_filename(); let Some(filename) = event_filename.file_name() else { - debug!("Creation event has no filename component: {}", event_filename.display()); + debug!( + "Creation event has no filename component: {}", + event_filename.display() + ); return Ok(()); }; let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) else { - debug!("Parent inode not in map, cannot construct host path for: {}", event_filename.display()); + debug!( + "Parent inode not in map, cannot construct host path for: {}", + event_filename.display() + ); return Ok(()); }; @@ -242,8 +251,16 @@ impl HostScanner { ); self.update_entry_with_inode(inode, host_path) - .with_context(|| format!("Failed to add creation event entry for {}", event_filename.display()))?; - debug!("Successfully added inode entry for newly created file: {}", event_filename.display()); + .with_context(|| { + format!( + "Failed to add creation event entry for {}", + event_filename.display() + ) + })?; + debug!( + "Successfully added inode entry for newly created file: {}", + event_filename.display() + ); Ok(()) } From 283f50a8e1ab913a68bd36a583798ce915f5fb03 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 14:56:19 -0700 Subject: [PATCH 10/22] Refactored update_entry_with_inode and update_entry --- fact/src/host_scanner.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index d94824b5..f8ba628e 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -159,32 +159,22 @@ impl HostScanner { dev: metadata.st_dev(), }; - self.kernel_inode_map - .borrow_mut() - .insert(inode, 0, 0) - .with_context(|| format!("Failed to insert kernel entry for {}", path.display()))?; - let mut inode_map = self.inode_map.borrow_mut(); - let entry = inode_map.entry(inode).or_default(); - *entry = host_info::remove_host_mount(path); - - self.metrics.scan_inc(ScanLabels::FileUpdated); + let host_path = host_info::remove_host_mount(path); + self.update_entry_with_inode(&inode, host_path)?; debug!("Added entry for {}: {inode:?}", path.display()); Ok(()) } - // Similar to update_entry except we are are directly using the inode instead of the path. + /// Similar to update_entry except we are are directly using the inode instead of the path. fn update_entry_with_inode(&self, inode: &inode_key_t, path: PathBuf) -> anyhow::Result<()> { - debug!("Adding entry for {}: {inode:?}", path.display()); - self.kernel_inode_map .borrow_mut() .insert(*inode, 0, 0) .with_context(|| format!("Failed to insert kernel entry for {}", path.display()))?; + let mut inode_map = self.inode_map.borrow_mut(); let entry = inode_map.entry(*inode).or_default(); - // Not removing the host mount, which is done in update_entry. - // I am not sure if that is correct. *entry = path; self.metrics.scan_inc(ScanLabels::FileUpdated); From 600d08c0c1c9c76fd4fba8f227e636615a197a26 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 19 Mar 2026 15:02:06 -0700 Subject: [PATCH 11/22] Removed tests/test_inode_tracking.py --- tests/test_inode_tracking.py | 79 ------------------------------------ 1 file changed, 79 deletions(-) delete mode 100644 tests/test_inode_tracking.py diff --git a/tests/test_inode_tracking.py b/tests/test_inode_tracking.py deleted file mode 100644 index 2fa83440..00000000 --- a/tests/test_inode_tracking.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Test that verifies inode tracking for newly created files. - -Expected behavior: -1. File created in monitored directory -2. BPF adds inode to kernel map (if parent is monitored) -3. Creation event has non-zero inode -4. Subsequent events on that file should also have the inode populated -""" - -import os -from tempfile import NamedTemporaryFile - -import pytest -import yaml - -from event import Event, EventType, Process - - -@pytest.fixture -def fact_config(monitored_dir, logs_dir): - """ - Config that includes both the directory and its contents. - This ensures the parent directory inode is tracked. - """ - cwd = os.getcwd() - config = { - 'paths': [f'{monitored_dir}', f'{monitored_dir}/*', f'{monitored_dir}/**', '/mounted/**', '/container-dir/**'], - 'grpc': { - 'url': 'http://127.0.0.1:9999', - }, - 'endpoint': { - 'address': '127.0.0.1:9000', - 'expose_metrics': True, - 'health_check': True, - }, - 'json': True, - } - config_file = NamedTemporaryFile( - prefix='fact-config-', suffix='.yml', dir=cwd, mode='w') - yaml.dump(config, config_file) - - yield config, config_file.name - with open(os.path.join(logs_dir, 'fact.yml'), 'w') as f: - with open(config_file.name, 'r') as r: - f.write(r.read()) - config_file.close() - - -def test_inode_tracking_on_creation(monitored_dir, test_file, server): - """ - Test that when a file is created in a monitored directory, - its inode is added to the tracking map. - - The test_file fixture ensures the directory exists and has content - when fact starts, so the parent directory inode gets tracked. - """ - # Create a new file - fut = os.path.join(monitored_dir, 'new_file.txt') - with open(fut, 'w') as f: - f.write('initial content') - - # Wait for creation event - process = Process.from_proc() - creation_event = Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=fut) - - server.wait_events([creation_event]) - - # Now modify the file - the inode should be tracked from creation - with open(fut, 'a') as f: - f.write('appended content') - - # This open event should have host_path populated because the inode - # was added to the map during creation - open_event = Event(process=process, event_type=EventType.OPEN, - file=fut, host_path=fut) - - server.wait_events([open_event]) From 1d1e383556f0be64bbab515735de96deba2e4bb0 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 20 Mar 2026 12:49:22 -0700 Subject: [PATCH 12/22] Removed debugging and refactored handle_creation_event --- fact/src/host_scanner.rs | 61 +++++++++------------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index f8ba628e..1adf94ff 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -199,58 +199,23 @@ impl HostScanner { /// path by appending the new file's name. fn handle_creation_event(&self, event: &Event) -> anyhow::Result<()> { let inode = event.get_inode(); - - if self.get_host_path(Some(inode)).is_some() { - return Ok(()); - } - let parent_inode = event.get_parent_inode(); - - if parent_inode.empty() { - debug!( - "Creation event has no parent inode: {}", - event.get_filename().display() - ); + if self.get_host_path(Some(inode)).is_some() || parent_inode.empty() { return Ok(()); } - let event_filename = event.get_filename(); - let Some(filename) = event_filename.file_name() else { - debug!( - "Creation event has no filename component: {}", - event_filename.display() - ); - return Ok(()); - }; - - let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) else { - debug!( - "Parent inode not in map, cannot construct host path for: {}", - event_filename.display() - ); - return Ok(()); - }; - - let host_path = parent_host_path.join(filename); - - debug!( - "Constructed host path for creation event: {} (from container path: {}, parent host path: {})", - host_path.display(), - event_filename.display(), - parent_host_path.display() - ); - - self.update_entry_with_inode(inode, host_path) - .with_context(|| { - format!( - "Failed to add creation event entry for {}", - event_filename.display() - ) - })?; - debug!( - "Successfully added inode entry for newly created file: {}", - event_filename.display() - ); + if let Some(filename) = event.get_filename().file_name() { + if 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() + ) + })?; + } + } Ok(()) } From 65f53a09008efd4f6f0e3e8e3ebbb861ea4cf6e4 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 20 Mar 2026 16:27:58 -0700 Subject: [PATCH 13/22] Not expecting host_path for ignored directories --- tests/conftest.py | 7 +++--- tests/test_config_hotreload.py | 2 +- tests/test_editors/test_nvim.py | 21 +++++++++--------- tests/test_editors/test_sed.py | 12 +++++----- tests/test_editors/test_vi.py | 39 ++++++++++++++++----------------- tests/test_editors/test_vim.py | 37 ++++++++++++++----------------- tests/test_wildcard.py | 24 ++++++++++++++------ 7 files changed, 73 insertions(+), 69 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cfb6be05..826717b5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,12 +106,11 @@ def fact_config(request, monitored_dir, logs_dir): config = { 'paths': [ f'{monitored_dir}', - f'{monitored_dir}/*', - f'{monitored_dir}/**', + f'{monitored_dir}/**/*', '/mounted', - '/mounted/**', + '/mounted/**/*', '/container-dir', - '/container-dir/**', + '/container-dir/**/*', ], 'grpc': { 'url': 'http://127.0.0.1:9999', diff --git a/tests/test_config_hotreload.py b/tests/test_config_hotreload.py index d276582f..20942fde 100644 --- a/tests/test_config_hotreload.py +++ b/tests/test_config_hotreload.py @@ -136,7 +136,7 @@ def test_paths(fact, fact_config, monitored_dir, ignored_dir, server): server.wait_events([e]) config, config_file = fact_config - config['paths'] = [f'{ignored_dir}/**/*'] + config['paths'] = [f'{ignored_dir}', f'{ignored_dir}/**/*'] reload_config(fact, config, config_file) # At this point, the event in the ignored directory should show up diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 4afcf7dc..8332b64e 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -25,9 +25,7 @@ def test_new_file(editor_container, server): def test_open_file(editor_container, server, ignored_dir): fut = '/mounted/test.txt' - fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' - fut_backup_host = f'{ignored_dir}/test.txt~' cmd = f"nvim {fut} '+:normal iThis is a test' -c x" container_id = editor_container.id[:12] @@ -49,25 +47,26 @@ def test_open_file(editor_container, server, ignored_dir): ) vi_test_file = get_vi_test_file('/mounted') - vi_test_file_host = get_vi_test_file(ignored_dir) + # TODO: host_path is empty for creation events in bind-mounted directories + # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=nvim, event_type=EventType.CREATION, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=nvim, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), + file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=nvim, event_type=EventType.UNLINK, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=nvim, event_type=EventType.RENAME, - file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=nvim, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=nvim, event_type=EventType.PERMISSION, - file=fut, host_path=fut_host, mode=0o100644), + file=fut, host_path='', mode=0o100644), Event(process=nvim, event_type=EventType.UNLINK, - file=fut_backup, host_path=fut_backup_host), + file=fut_backup, host_path=''), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 579e6d13..9d241e0c 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -5,7 +5,6 @@ def test_sed(vi_container, server, ignored_dir): # File Under Test fut = '/mounted/test.txt' - fut_host = f'{ignored_dir}/test.txt' create_cmd = f"sh -c \"echo 'This is a test' > {fut}\"" sed_cmd = fr'sed -i -e "s/a test/not \\0/" {fut}' container_id = vi_container.id[:12] @@ -27,17 +26,18 @@ def test_sed(vi_container, server, ignored_dir): ) sed_tmp_file = re.compile(r'\/mounted\/sed[0-9a-zA-Z]{6}') - sed_tmp_host = re.compile(rf'{re.escape(ignored_dir)}\/sed[0-9a-zA-Z]{{6}}') + # TODO: host_path is empty for creation events in bind-mounted directories + # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=shell, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=sed, event_type=EventType.CREATION, - file=sed_tmp_file, host_path=sed_tmp_host), + file=sed_tmp_file, host_path=''), Event(process=sed, event_type=EventType.OWNERSHIP, - file=sed_tmp_file, host_path=sed_tmp_host, owner_uid=0, owner_gid=0), + file=sed_tmp_file, host_path='', owner_uid=0, owner_gid=0), Event(process=sed, event_type=EventType.RENAME, - file=fut, host_path=fut_host, old_file=sed_tmp_file, old_host_path=sed_tmp_host), + file=fut, host_path='', old_file=sed_tmp_file, old_host_path=''), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index c576dfd5..40dcbc49 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -19,6 +19,8 @@ def test_new_file(vi_container, server): container_id=vi_container.id[:12], ) + # TODO: host_path is empty for creation events in bind-mounted directories + # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), @@ -86,15 +88,10 @@ def test_new_file_ovfs(vi_container, server): def test_open_file(vi_container, server, ignored_dir): fut = '/mounted/test.txt' - fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' - fut_backup_host = f'{ignored_dir}/test.txt~' swap_file = '/mounted/.test.txt.swp' - swap_file_host = f'{ignored_dir}/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' - swx_file_host = f'{ignored_dir}/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') - vi_test_file_host = get_vi_test_file(ignored_dir) exe = '/usr/bin/vi' container_id = vi_container.id[:12] @@ -117,37 +114,39 @@ def test_open_file(vi_container, server, ignored_dir): container_id=container_id, ) + # TODO: host_path is empty for creation events in bind-mounted directories + # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch_process, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swx_file, host_path=swx_file_host), + file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swx_file, host_path=swx_file_host), + file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, - file=swap_file, host_path=swap_file_host, mode=0o644), + file=swap_file, host_path='', mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), + file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.RENAME, - file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, - file=fut, host_path=fut_host, mode=0o100644), + file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=fut_backup, host_path=fut_backup_host), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 56ecb5c2..bdeae602 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -82,15 +82,10 @@ def test_new_file_ovfs(editor_container, server): def test_open_file(editor_container, server, ignored_dir): fut = '/mounted/test.txt' - fut_host = f'{ignored_dir}/test.txt' fut_backup = f'{fut}~' - fut_backup_host = f'{ignored_dir}/test.txt~' swap_file = '/mounted/.test.txt.swp' - swap_file_host = f'{ignored_dir}/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' - swx_file_host = f'{ignored_dir}/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') - vi_test_file_host = get_vi_test_file(ignored_dir) container_id = editor_container.id[:12] cmd = f"vim {fut} '+:normal iThis is a test' -c x" @@ -112,37 +107,39 @@ def test_open_file(editor_container, server, ignored_dir): container_id=container_id, ) + # TODO: host_path is empty for creation events in bind-mounted directories + # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch_process, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swx_file, host_path=swx_file_host), + file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swx_file, host_path=swx_file_host), + file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, - file=swap_file, host_path=swap_file_host, mode=0o644), + file=swap_file, host_path='', mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.OWNERSHIP, - file=vi_test_file, host_path=vi_test_file_host, owner_uid=0, owner_gid=0), + file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, - file=vi_test_file, host_path=vi_test_file_host), + file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.RENAME, - file=fut_backup, host_path=fut_backup_host, old_file=fut, old_host_path=fut_host), + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, - file=fut, host_path=fut_host), + file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, - file=fut, host_path=fut_host, mode=0o100644), + file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=fut_backup, host_path=fut_backup_host), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, - file=swap_file, host_path=swap_file_host), + file=swap_file, host_path=''), ] server.wait_events(events, strict=True) diff --git a/tests/test_wildcard.py b/tests/test_wildcard.py index aa33c0e1..a15821bf 100644 --- a/tests/test_wildcard.py +++ b/tests/test_wildcard.py @@ -36,8 +36,10 @@ def test_extension_wildcard(wildcard_config, monitored_dir, server): with open(txt_file, 'w') as f: f.write('This should be captured') + # TODO: host_path is empty because wildcard patterns don't include the parent + # directory, so the parent inode isn't tracked for path construction e = Event(process=process, event_type=EventType.CREATION, - file=txt_file, host_path=txt_file) + file=txt_file, host_path='') server.wait_events([e]) @@ -54,8 +56,10 @@ def test_prefix_wildcard(wildcard_config, monitored_dir, server): with open(test_log, 'w') as f: f.write('This should be captured') + # TODO: host_path is empty because wildcard patterns don't include the parent + # directory, so the parent inode isn't tracked for path construction e = Event(process=process, event_type=EventType.CREATION, - file=test_log, host_path=test_log) + file=test_log, host_path='') server.wait_events([e]) @@ -79,11 +83,13 @@ def test_recursive_wildcard(wildcard_config, monitored_dir, server): with open(nested_txt, 'w') as f: f.write('Nested txt') + # TODO: host_path is empty because wildcard patterns don't include the parent + # directory, so the parent inode isn't tracked for path construction events = [ Event(process=process, event_type=EventType.CREATION, - file=root_txt, host_path=root_txt), + file=root_txt, host_path=''), Event(process=process, event_type=EventType.CREATION, - file=nested_txt, host_path=nested_txt), + file=nested_txt, host_path=''), ] server.wait_events(events) @@ -96,8 +102,10 @@ def test_nonrecursive_wildcard(wildcard_config, monitored_dir, server): with open(fut, 'w') as f: f.write('This should be captured') + # TODO: host_path is empty because wildcard patterns don't include the parent + # directory, so the parent inode isn't tracked for path construction e = Event(process=process, event_type=EventType.CREATION, - file=fut, host_path=fut) + file=fut, host_path='') server.wait_events([e]) @@ -118,11 +126,13 @@ def test_multiple_patterns(wildcard_config, monitored_dir, server): with open(log_file, 'w') as f: f.write('Log file') + # TODO: host_path is empty because wildcard patterns don't include the parent + # directory, so the parent inode isn't tracked for path construction events = [ Event(process=process, event_type=EventType.CREATION, - file=txt_file, host_path=txt_file), + file=txt_file, host_path=''), Event(process=process, event_type=EventType.CREATION, - file=log_file, host_path=log_file), + file=log_file, host_path=''), ] server.wait_events(events) From 950c4023465030be6c344d8393013f0a453c8d10 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 20 Mar 2026 16:39:52 -0700 Subject: [PATCH 14/22] Fixed style. Removed unused ignored_dir --- fact/src/host_scanner.rs | 16 ++++++++-------- tests/test_editors/test_nvim.py | 2 +- tests/test_editors/test_sed.py | 2 +- tests/test_editors/test_vi.py | 2 +- tests/test_editors/test_vim.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 1adf94ff..da0e652b 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -206,14 +206,14 @@ impl HostScanner { if let Some(filename) = event.get_filename().file_name() { if 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() - ) - })?; + 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() + ) + })?; } } diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 8332b64e..101b3188 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -23,7 +23,7 @@ def test_new_file(editor_container, server): server.wait_events(events, strict=True) -def test_open_file(editor_container, server, ignored_dir): +def test_open_file(editor_container, server): fut = '/mounted/test.txt' fut_backup = f'{fut}~' cmd = f"nvim {fut} '+:normal iThis is a test' -c x" diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 9d241e0c..27a36fb3 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -2,7 +2,7 @@ from event import Event, EventType, Process -def test_sed(vi_container, server, ignored_dir): +def test_sed(vi_container, server): # File Under Test fut = '/mounted/test.txt' create_cmd = f"sh -c \"echo 'This is a test' > {fut}\"" diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index 40dcbc49..be52eaba 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -86,7 +86,7 @@ def test_new_file_ovfs(vi_container, server): server.wait_events(events, strict=True) -def test_open_file(vi_container, server, ignored_dir): +def test_open_file(vi_container, server): fut = '/mounted/test.txt' fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index bdeae602..aa453c6c 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -80,7 +80,7 @@ def test_new_file_ovfs(editor_container, server): server.wait_events(events, strict=True) -def test_open_file(editor_container, server, ignored_dir): +def test_open_file(editor_container, server): fut = '/mounted/test.txt' fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' From a8471b009b74c5534753560de581d1e2fa8f1551 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 22 Mar 2026 18:26:39 -0700 Subject: [PATCH 15/22] Logic for checking if a file should be monitored moved to is_monitored --- fact-ebpf/src/bpf/file.h | 28 +++++++++++++++++----------- fact-ebpf/src/bpf/inode.h | 3 ++- fact-ebpf/src/bpf/main.c | 25 +++++++++++-------------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/fact-ebpf/src/bpf/file.h b/fact-ebpf/src/bpf/file.h index bb131f80..68cb860b 100644 --- a/fact-ebpf/src/bpf/file.h +++ b/fact-ebpf/src/bpf/file.h @@ -27,18 +27,24 @@ __always_inline static bool path_is_monitored(struct bound_path_t* path) { return res; } -__always_inline static bool is_monitored(inode_key_t inode, struct bound_path_t* path, inode_key_t** submit) { +__always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct bound_path_t* path, const inode_key_t* parent, inode_key_t** submit) { const inode_value_t* volatile inode_value = inode_get(&inode); - switch (inode_is_monitored(inode_value)) { - case NOT_MONITORED: - *submit = NULL; - if (path_is_monitored(path)) { - return true; - } - return false; - case MONITORED: - break; + if (inode_is_monitored(inode_value) == MONITORED) { + return MONITORED; } - return true; + + if (parent != NULL) { + const inode_value_t* volatile parent_value = inode_get(parent); + if (inode_is_monitored(parent_value) == MONITORED) { + return PARENT_MONITORED; + } + } + + *submit = NULL; + if (path_is_monitored(path)) { + return MONITORED; + } + + return NOT_MONITORED; } diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index a247ec28..a29043a3 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -58,7 +58,7 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { return key; } -__always_inline static inode_value_t* inode_get(struct inode_key_t* inode) { +__always_inline static inode_value_t* inode_get(const struct inode_key_t* inode) { if (inode == NULL) { return NULL; } @@ -83,6 +83,7 @@ __always_inline static long inode_remove(struct inode_key_t* inode) { typedef enum inode_monitored_t { NOT_MONITORED = 0, MONITORED, + PARENT_MONITORED, } inode_monitored_t; /** diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index cc877c68..b7c044f1 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -47,20 +47,17 @@ int BPF_PROG(trace_file_open, struct file* file) { inode_key_t inode_key = inode_to_key(file->f_inode); inode_key_t* inode_to_submit = &inode_key; - // Extract parent inode struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; inode_key_t parent_key = inode_to_key(parent_inode_ptr); - // For file creation events, check if the parent directory is being - // monitored. If so, add the new file's inode to the tracked set. - if (event_type == FILE_ACTIVITY_CREATION) { - if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { - inode_add(&inode_key); - } + inode_monitored_t status = is_monitored(inode_key, path, &parent_key, &inode_to_submit); + + if (status == PARENT_MONITORED && event_type == FILE_ACTIVITY_CREATION) { + inode_add(&inode_key); } - if (!is_monitored(inode_key, path, &inode_to_submit)) { + if (status == NOT_MONITORED) { goto ignored; } @@ -92,7 +89,7 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { inode_key_t inode_key = inode_to_key(dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (!is_monitored(inode_key, path, &inode_to_submit)) { + if (is_monitored(inode_key, path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_unlink.ignored++; return 0; } @@ -123,7 +120,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { + if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_chmod.ignored++; return 0; } @@ -161,7 +158,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (!is_monitored(inode_key, bound_path, &inode_to_submit)) { + if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_chown.ignored++; return 0; } @@ -211,10 +208,10 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, inode_key_t* old_inode_submit = &old_inode; inode_key_t* new_inode_submit = &new_inode; - bool old_monitored = is_monitored(old_inode, old_path, &old_inode_submit); - bool new_monitored = is_monitored(new_inode, new_path, &new_inode_submit); + inode_monitored_t old_monitored = is_monitored(old_inode, old_path, NULL, &old_inode_submit); + inode_monitored_t new_monitored = is_monitored(new_inode, new_path, NULL, &new_inode_submit); - if (!old_monitored && !new_monitored) { + if (old_monitored == NOT_MONITORED && new_monitored == NOT_MONITORED) { m->path_rename.ignored++; return 0; } From 5e145caa13d98ef3bafadc1e62e5082d1aca3d53 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 22 Mar 2026 18:44:09 -0700 Subject: [PATCH 16/22] Refactored so that NULL doesn't have to be passed to is_monitored many times --- fact-ebpf/src/bpf/file.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fact-ebpf/src/bpf/file.h b/fact-ebpf/src/bpf/file.h index 68cb860b..279f5777 100644 --- a/fact-ebpf/src/bpf/file.h +++ b/fact-ebpf/src/bpf/file.h @@ -27,7 +27,7 @@ __always_inline static bool path_is_monitored(struct bound_path_t* path) { return res; } -__always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct bound_path_t* path, const inode_key_t* parent, inode_key_t** submit) { +__always_inline static inode_monitored_t is_monitored_with_parent(inode_key_t inode, struct bound_path_t* path, const inode_key_t* parent, inode_key_t** submit) { const inode_value_t* volatile inode_value = inode_get(&inode); if (inode_is_monitored(inode_value) == MONITORED) { @@ -48,3 +48,7 @@ __always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct return NOT_MONITORED; } + +__always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct bound_path_t* path, inode_key_t** submit) { + return is_monitored_with_parent(inode, path, NULL, submit); +} From 01841805fde3d6e08cbe33da02e379bfdd47e389 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 22 Mar 2026 19:18:02 -0700 Subject: [PATCH 17/22] Removed unneeded TODOs --- fact-ebpf/src/bpf/main.c | 12 ++++++------ tests/test_editors/test_nvim.py | 2 -- tests/test_editors/test_sed.py | 2 -- tests/test_editors/test_vi.py | 4 ---- tests/test_editors/test_vim.py | 2 -- 5 files changed, 6 insertions(+), 16 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index b7c044f1..8c379a83 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -51,7 +51,7 @@ int BPF_PROG(trace_file_open, struct file* file) { struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; inode_key_t parent_key = inode_to_key(parent_inode_ptr); - inode_monitored_t status = is_monitored(inode_key, path, &parent_key, &inode_to_submit); + inode_monitored_t status = is_monitored_with_parent(inode_key, path, &parent_key, &inode_to_submit); if (status == PARENT_MONITORED && event_type == FILE_ACTIVITY_CREATION) { inode_add(&inode_key); @@ -89,7 +89,7 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { inode_key_t inode_key = inode_to_key(dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, path, NULL, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, path, &inode_to_submit) == NOT_MONITORED) { m->path_unlink.ignored++; return 0; } @@ -120,7 +120,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, bound_path, &inode_to_submit) == NOT_MONITORED) { m->path_chmod.ignored++; return 0; } @@ -158,7 +158,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, bound_path, &inode_to_submit) == NOT_MONITORED) { m->path_chown.ignored++; return 0; } @@ -208,8 +208,8 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, inode_key_t* old_inode_submit = &old_inode; inode_key_t* new_inode_submit = &new_inode; - inode_monitored_t old_monitored = is_monitored(old_inode, old_path, NULL, &old_inode_submit); - inode_monitored_t new_monitored = is_monitored(new_inode, new_path, NULL, &new_inode_submit); + inode_monitored_t old_monitored = is_monitored(old_inode, old_path, &old_inode_submit); + inode_monitored_t new_monitored = is_monitored(new_inode, new_path, &new_inode_submit); if (old_monitored == NOT_MONITORED && new_monitored == NOT_MONITORED) { m->path_rename.ignored++; diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 101b3188..9f3d095d 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -48,8 +48,6 @@ def test_open_file(editor_container, server): vi_test_file = get_vi_test_file('/mounted') - # TODO: host_path is empty for creation events in bind-mounted directories - # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 27a36fb3..c3f6a2f8 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -27,8 +27,6 @@ def test_sed(vi_container, server): sed_tmp_file = re.compile(r'\/mounted\/sed[0-9a-zA-Z]{6}') - # TODO: host_path is empty for creation events in bind-mounted directories - # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=shell, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index be52eaba..4301890a 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -19,8 +19,6 @@ def test_new_file(vi_container, server): container_id=vi_container.id[:12], ) - # TODO: host_path is empty for creation events in bind-mounted directories - # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), @@ -114,8 +112,6 @@ def test_open_file(vi_container, server): container_id=container_id, ) - # TODO: host_path is empty for creation events in bind-mounted directories - # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch_process, event_type=EventType.CREATION, file=fut, host_path=''), diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index aa453c6c..56cbb667 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -107,8 +107,6 @@ def test_open_file(editor_container, server): container_id=container_id, ) - # TODO: host_path is empty for creation events in bind-mounted directories - # because the host-side parent directory (ignored_dir) is not scanned events = [ Event(process=touch_process, event_type=EventType.CREATION, file=fut, host_path=''), From 2b139ffd605a1e726e2c57b754e65514f8ca08f4 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 07:46:28 -0700 Subject: [PATCH 18/22] Fixed format after rebasing and updating edition to 2024 --- fact/src/host_scanner.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index da0e652b..fdcbfecf 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -204,17 +204,17 @@ impl HostScanner { return Ok(()); } - if let Some(filename) = event.get_filename().file_name() { - if 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 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() + ) + })?; } Ok(()) From 93dd1d82181cafb72614961b5863fec30c335339 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 08:57:43 -0700 Subject: [PATCH 19/22] Collapsed if statements --- fact/src/host_scanner.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index fdcbfecf..f8ff5136 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -262,11 +262,10 @@ impl HostScanner { self.metrics.events.added(); // Handle file creation events by adding new inodes to the map - if event.is_creation() { - if let Err(e) = self.handle_creation_event(&event) { + if event.is_creation() && + let Err(e) = self.handle_creation_event(&event) { warn!("Failed to handle creation event: {e}"); } - } if let Some(host_path) = self.get_host_path(Some(event.get_inode())) { self.metrics.scan_inc(ScanLabels::InodeHit); From b411601cc6862074d3fe4c12d45f6dd8528eb786 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 18:50:11 -0700 Subject: [PATCH 20/22] Moves some of the logic in is_monitored_with_parent to inode_is_monitored and removed is_monitored_with_parent --- fact-ebpf/src/bpf/file.h | 19 +++++-------------- fact-ebpf/src/bpf/inode.h | 15 ++++++--------- fact-ebpf/src/bpf/main.c | 12 ++++++------ 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/fact-ebpf/src/bpf/file.h b/fact-ebpf/src/bpf/file.h index 279f5777..d0fdc8b1 100644 --- a/fact-ebpf/src/bpf/file.h +++ b/fact-ebpf/src/bpf/file.h @@ -27,18 +27,13 @@ __always_inline static bool path_is_monitored(struct bound_path_t* path) { return res; } -__always_inline static inode_monitored_t is_monitored_with_parent(inode_key_t inode, struct bound_path_t* path, const inode_key_t* parent, inode_key_t** submit) { +__always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct bound_path_t* path, const inode_key_t* parent, inode_key_t** submit) { const inode_value_t* volatile inode_value = inode_get(&inode); + const inode_value_t* volatile parent_value = inode_get(parent); - if (inode_is_monitored(inode_value) == MONITORED) { - return MONITORED; - } - - if (parent != NULL) { - const inode_value_t* volatile parent_value = inode_get(parent); - if (inode_is_monitored(parent_value) == MONITORED) { - return PARENT_MONITORED; - } + inode_monitored_t status = inode_is_monitored(inode_value, parent_value); + if (status != NOT_MONITORED) { + return status; } *submit = NULL; @@ -48,7 +43,3 @@ __always_inline static inode_monitored_t is_monitored_with_parent(inode_key_t in return NOT_MONITORED; } - -__always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct bound_path_t* path, inode_key_t** submit) { - return is_monitored_with_parent(inode, path, NULL, submit); -} diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index a29043a3..481313e3 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -86,19 +86,16 @@ typedef enum inode_monitored_t { PARENT_MONITORED, } inode_monitored_t; -/** - * Check if the provided inode is being monitored. - * - * The current implementation is very basic and might seem like - * overkill, but in the near future this function will be extended to - * check if the parent of the provided inode is monitored and provide - * different results for handling more complicated scenarios. - */ -__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* volatile inode) { +// Check if the provided inode or its parent is being monitored. +__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* volatile inode, const inode_value_t* volatile parent_inode) { if (inode != NULL) { return MONITORED; } + if (parent_inode != NULL) { + return PARENT_MONITORED; + } + return NOT_MONITORED; } diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 8c379a83..b7c044f1 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -51,7 +51,7 @@ int BPF_PROG(trace_file_open, struct file* file) { struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; inode_key_t parent_key = inode_to_key(parent_inode_ptr); - inode_monitored_t status = is_monitored_with_parent(inode_key, path, &parent_key, &inode_to_submit); + inode_monitored_t status = is_monitored(inode_key, path, &parent_key, &inode_to_submit); if (status == PARENT_MONITORED && event_type == FILE_ACTIVITY_CREATION) { inode_add(&inode_key); @@ -89,7 +89,7 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { inode_key_t inode_key = inode_to_key(dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, path, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_unlink.ignored++; return 0; } @@ -120,7 +120,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, bound_path, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_chmod.ignored++; return 0; } @@ -158,7 +158,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign inode_key_t inode_key = inode_to_key(path->dentry->d_inode); inode_key_t* inode_to_submit = &inode_key; - if (is_monitored(inode_key, bound_path, &inode_to_submit) == NOT_MONITORED) { + if (is_monitored(inode_key, bound_path, NULL, &inode_to_submit) == NOT_MONITORED) { m->path_chown.ignored++; return 0; } @@ -208,8 +208,8 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, inode_key_t* old_inode_submit = &old_inode; inode_key_t* new_inode_submit = &new_inode; - inode_monitored_t old_monitored = is_monitored(old_inode, old_path, &old_inode_submit); - inode_monitored_t new_monitored = is_monitored(new_inode, new_path, &new_inode_submit); + inode_monitored_t old_monitored = is_monitored(old_inode, old_path, NULL, &old_inode_submit); + inode_monitored_t new_monitored = is_monitored(new_inode, new_path, NULL, &new_inode_submit); if (old_monitored == NOT_MONITORED && new_monitored == NOT_MONITORED) { m->path_rename.ignored++; From 89a054209b0fef3fe790ab85f67175cf11811d10 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 19:13:48 -0700 Subject: [PATCH 21/22] Passing inode by value instead of reference to update_entry_with_inode --- fact/src/host_scanner.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index f8ff5136..36cacdef 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -160,21 +160,21 @@ impl HostScanner { }; let host_path = host_info::remove_host_mount(path); - self.update_entry_with_inode(&inode, host_path)?; + self.update_entry_with_inode(inode, host_path)?; debug!("Added entry for {}: {inode:?}", path.display()); Ok(()) } /// Similar to update_entry except we are are directly using the inode instead of the path. - fn update_entry_with_inode(&self, inode: &inode_key_t, path: PathBuf) -> anyhow::Result<()> { + fn update_entry_with_inode(&self, inode: inode_key_t, path: PathBuf) -> anyhow::Result<()> { self.kernel_inode_map .borrow_mut() - .insert(*inode, 0, 0) + .insert(inode, 0, 0) .with_context(|| format!("Failed to insert kernel entry for {}", path.display()))?; let mut inode_map = self.inode_map.borrow_mut(); - let entry = inode_map.entry(*inode).or_default(); + let entry = inode_map.entry(inode).or_default(); *entry = path; self.metrics.scan_inc(ScanLabels::FileUpdated); @@ -208,7 +208,7 @@ impl HostScanner { && 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) + self.update_entry_with_inode(*inode, host_path) .with_context(|| { format!( "Failed to add creation event entry for {}", From 54be631e1743316bb53bf0bbf6f96544788e131c Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 23 Mar 2026 20:30:03 -0700 Subject: [PATCH 22/22] Removed two unneeded paths from tests/conftest.py --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 826717b5..165649f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,9 +107,7 @@ def fact_config(request, monitored_dir, logs_dir): 'paths': [ f'{monitored_dir}', f'{monitored_dir}/**/*', - '/mounted', '/mounted/**/*', - '/container-dir', '/container-dir/**/*', ], 'grpc': {