From 8fb3a4b42296cb4da3c153fcda2b54d51807fcf5 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Tue, 18 Nov 2025 11:47:38 +0100 Subject: [PATCH 01/16] Attach to the chown LSM hook --- fact-ebpf/src/bpf/events.h | 22 +++++++++++++++ fact-ebpf/src/bpf/main.c | 55 ++++++++++++++++++++++++++++++++++++++ fact-ebpf/src/bpf/types.h | 8 ++++++ 3 files changed, 85 insertions(+) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 4e9e51bf..a4c1225d 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -73,3 +73,25 @@ __always_inline static void submit_mode_event(struct metrics_by_hook_t* m, __submit_event(event, m, FILE_ACTIVITY_CHMOD, filename, inode, use_bpf_d_path); } + +__always_inline static void submit_owner_event(struct metrics_by_hook_t* m, + const char filename[PATH_MAX], + inode_key_t* inode, + unsigned long long uid, + unsigned long long gid, + unsigned long long old_uid, + unsigned long long old_gid, + bool use_bpf_d_path) { + struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); + if (event == NULL) { + m->ringbuffer_full++; + return; + } + + event->chown.new.uid = uid; + event->chown.new.gid = gid; + event->chown.old.uid = old_uid; + event->chown.old.gid = old_gid; + + __submit_event(event, m, FILE_ACTIVITY_CHOWN, filename, inode, use_bpf_d_path); +} diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 2e4d64c0..f95e78a1 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -173,3 +173,58 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { return 0; } + +SEC("lsm/path_chown") +int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsigned long long gid) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->path_chown.total++; + + struct bound_path_t* bound_path = NULL; + if (path_hooks_support_bpf_d_path) { + bound_path = path_read(path); + } else { + bound_path = path_read_no_d_path(path); + } + + if (bound_path == NULL) { + bpf_printk("Failed to read path"); + m->path_chown.error++; + return 0; + } + + inode_key_t inode_key = inode_to_key(path->dentry->d_inode); + const inode_value_t* inode = inode_get(&inode_key); + + switch (inode_is_monitored(inode)) { + case NOT_MONITORED: + if (!is_monitored(bound_path)) { + m->path_chown.ignored++; + return 0; + } + break; + + case MONITORED: + break; + } + + struct dentry* d = BPF_CORE_READ(path, dentry); + kuid_t kuid = BPF_CORE_READ(d, d_inode, i_uid); + kgid_t kgid = BPF_CORE_READ(d, d_inode, i_gid); + unsigned long long old_uid = BPF_CORE_READ(&kuid, val); + unsigned long long old_gid = BPF_CORE_READ(&kgid, val); + + submit_owner_event(&m->path_chown, + bound_path->path, + &inode_key, + uid, + gid, + old_uid, + old_gid, + path_hooks_support_bpf_d_path); + + return 0; +} diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index f0fc2a94..8fce112e 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -53,6 +53,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_CREATION, FILE_ACTIVITY_UNLINK, FILE_ACTIVITY_CHMOD, + FILE_ACTIVITY_CHOWN, } file_activity_type_t; struct event_t { @@ -66,6 +67,12 @@ struct event_t { short unsigned int new; short unsigned int old; } chmod; + struct { + struct { + unsigned int uid; + unsigned int gid; + } old, new; + } chown; }; }; @@ -96,4 +103,5 @@ struct metrics_t { struct metrics_by_hook_t file_open; struct metrics_by_hook_t path_unlink; struct metrics_by_hook_t path_chmod; + struct metrics_by_hook_t path_chown; }; From 29b8c6e2e8fea5b96fbe465c4daf437653458c6b Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Tue, 18 Nov 2025 15:42:10 +0100 Subject: [PATCH 02/16] Userland bits missing user/group mapping to string representation --- fact/src/event/mod.rs | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 784458d7..089d98a3 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -82,6 +82,7 @@ impl Event { FileData::Creation(data) => &data.inode, FileData::Unlink(data) => &data.inode, FileData::Chmod(data) => &data.inner.inode, + FileData::Chown(data) => &data.inner.inode, } } @@ -91,6 +92,7 @@ impl Event { FileData::Creation(data) => data.host_file = host_path, FileData::Unlink(data) => data.host_file = host_path, FileData::Chmod(data) => data.inner.host_file = host_path, + FileData::Chown(data) => data.inner.host_file = host_path, } } } @@ -145,6 +147,7 @@ pub enum FileData { Creation(BaseFileData), Unlink(BaseFileData), Chmod(ChmodFileData), + Chown(ChownFileData), } impl FileData { @@ -167,6 +170,16 @@ impl FileData { }; FileData::Chmod(data) } + file_activity_type_t::FILE_ACTIVITY_CHOWN => { + let data = ChownFileData { + inner, + new_uid: unsafe { extra_data.chown.new.uid }, + new_gid: unsafe { extra_data.chown.new.gid }, + old_uid: unsafe { extra_data.chown.old.uid }, + old_gid: unsafe { extra_data.chown.old.gid }, + }; + FileData::Chown(data) + } invalid => unreachable!("Invalid event type: {invalid:?}"), }; @@ -196,6 +209,10 @@ impl From for fact_api::file_activity::File { let f_act = fact_api::FilePermissionChange::from(event); fact_api::file_activity::File::Permission(f_act) } + FileData::Chown(event) => { + let f_act = fact_api::FileOwnershipChange::from(event); + fact_api::file_activity::File::Ownership(f_act) + } } } } @@ -295,3 +312,52 @@ impl PartialEq for ChmodFileData { && self.inner == other.inner } } + +#[derive(Debug, Clone, Serialize)] +pub struct ChownFileData { + inner: BaseFileData, + new_uid: u32, + new_gid: u32, + old_uid: u32, + old_gid: u32, +} + +impl ChownFileData { + pub fn new( + filename: [c_char; PATH_MAX as usize], + inode: inode_key_t, + new_uid: u32, + new_gid: u32, + old_uid: u32, + old_gid: u32, + ) -> anyhow::Result { + let file = BaseFileData::new(filename, inode)?; + + Ok(ChownFileData { + inner: file, + new_uid, + new_gid, + old_uid, + old_gid, + }) + } +} +impl From for fact_api::FileOwnershipChange { + fn from(value: ChownFileData) -> Self { + let ChownFileData { + inner: file, + new_uid, + new_gid, + old_uid: _, + old_gid: _, + } = value; + let activity = fact_api::FileActivityBase::from(file); + fact_api::FileOwnershipChange { + activity: Some(activity), + uid: new_uid, + gid: new_gid, + username: "".to_string(), + group: "".to_string(), + } + } +} From dc4e60130185ad3ddb06182ce0bd5b637daf88af Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Tue, 13 Jan 2026 15:11:02 +0100 Subject: [PATCH 03/16] Harvest kernel metrics for chown --- fact/src/metrics/kernel_metrics.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 720943a9..1a6fc977 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -11,6 +11,7 @@ pub struct KernelMetrics { file_open: EventCounter, path_unlink: EventCounter, path_chmod: EventCounter, + path_chown: EventCounter, map: PerCpuArray, } @@ -31,15 +32,22 @@ impl KernelMetrics { "Events processed by the path_chmod LSM hook", &[], // Labels are not needed since `collect` will add them all ); + let path_chown = EventCounter::new( + "kernel_path_chown_events", + "Events processed by the path_chown LSM hook", + &[], // Labels are not needed since `collect` will add them all + ); file_open.register(reg); path_unlink.register(reg); path_chmod.register(reg); + path_chown.register(reg); KernelMetrics { file_open, path_unlink, path_chmod, + path_chown, map: kernel_metrics, } } @@ -87,6 +95,7 @@ impl KernelMetrics { KernelMetrics::refresh_labels(&self.file_open, &metrics.file_open); KernelMetrics::refresh_labels(&self.path_unlink, &metrics.path_unlink); KernelMetrics::refresh_labels(&self.path_chmod, &metrics.path_chmod); + KernelMetrics::refresh_labels(&self.path_chown, &metrics.path_chown); Ok(()) } From 16cc780daea775fa32a2a05a77a8abf34873471a Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Tue, 13 Jan 2026 17:56:01 +0100 Subject: [PATCH 04/16] Exclude rhcos 4.14 from the tests --- .github/workflows/unit-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 2df2428c..44887c5f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -71,6 +71,7 @@ jobs: # need more investigation - rhel-8 - rhcos-412-86-202402272018-0-gcp-x86-64 + - rhcos-414-92-202407091253-0-gcp-x86-64 # BPF trampolines are only implemented starting with RHEL 10 - rhel-9-arm64 EOF From b3e20ca2e618ca47ad266e56c0de2f3d04db8596 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 11 Dec 2025 10:46:00 +0100 Subject: [PATCH 05/16] Integration tests --- tests/event.py | 23 +++- tests/test_path_chown.py | 251 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 tests/test_path_chown.py diff --git a/tests/event.py b/tests/event.py index 5422833b..ce7b524a 100644 --- a/tests/event.py +++ b/tests/event.py @@ -31,6 +31,7 @@ class EventType(Enum): CREATION = 2 UNLINK = 3 PERMISSION = 4 + OWNERSHIP = 5 class Process: @@ -171,12 +172,16 @@ def __init__(self, event_type: EventType, file: str, host_path: str = '', - mode: int | None = None): + mode: int | None = None, + owner_uid: int | None = None, + owner_gid: int | None = None,): self._type: EventType = event_type self._process: Process = process self._file: str = file self._host_path: str = host_path self._mode: int | None = mode + self._owner_uid: int | None = owner_uid + self._owner_gid: int | None = owner_gid @property def event_type(self) -> EventType: @@ -198,6 +203,14 @@ def host_path(self) -> str: def mode(self) -> int | None: return self._mode + @property + def owner_uid(self) -> int | None: + return self._owner_uid + + @property + def owner_gid(self) -> int | None: + return self._owner_gid + @override def __eq__(self, other: Any) -> bool: if isinstance(other, FileActivity): @@ -217,6 +230,11 @@ def __eq__(self, other: Any) -> bool: return self.file == other.permission.activity.path and \ self.host_path == other.permission.activity.host_path and \ self.mode == other.permission.mode + elif self.event_type == EventType.OWNERSHIP: + return self.file == other.ownership.activity.path and \ + self.host_path == other.ownership.activity.host_path and \ + (self.owner_uid is None or self.owner_uid == other.ownership.uid) and \ + (self.owner_gid is None or self.owner_gid == other.ownership.gid) return False raise NotImplementedError @@ -229,6 +247,9 @@ def __str__(self) -> str: if self.event_type == EventType.PERMISSION: s += f', mode={self.mode}' + if self.event_type == EventType.OWNERSHIP: + s += f', owner=(uid={self.owner_uid}, gid={self.owner_gid})' + s += ')' return s diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py new file mode 100644 index 00000000..cf5d41d7 --- /dev/null +++ b/tests/test_path_chown.py @@ -0,0 +1,251 @@ +import os + +from event import Event, EventType, Process + +# Tests here have to use a container to do 'chown', +# otherwise they would require to run as root. + +# UID and GID values for chown tests +TEST_UID = 1234 +TEST_GID = 2345 + + +def test_chown(fact, test_container, server): + """ + Execute a chown operation on a file and verifies the corresponding event is + captured by the server. + + Args: + fact: Fixture for file activity (only required to be running). + test_container: A container for running commands in. + server: The server instance to communicate with. + """ + # File Under Test + fut = '/container-dir/test.txt' + + # Create the file and chown it + test_container.exec_run(f'touch {fut}') + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + + loginuid = pow(2, 32) - 1 + touch = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/touch', + args=f'touch {fut}', + name='touch', + container_id=test_container.id[:12], + loginuid=loginuid) + chown = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {fut}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + events = [ + Event(process=touch, event_type=EventType.CREATION, file=fut, + host_path=''), + Event(process=chown, event_type=EventType.OWNERSHIP, file=fut, + host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), + ] + + for e in events: + print(f'Waiting for event: {e}') + + server.wait_events(events) + + +def test_multiple(fact, test_container, server): + """ + Tests ownership operations on multiple files and verifies the corresponding + events are captured by the server. + + Args: + fact: Fixture for file activity (only required to be running). + test_container: A container for running commands in. + server: The server instance to communicate with. + """ + events = [] + loginuid = pow(2, 32) - 1 + + # File Under Test + for i in range(3): + fut = f'/container-dir/{i}.txt' + test_container.exec_run(f'touch {fut}') + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + + touch = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/touch', + args=f'touch {fut}', + name='touch', + container_id=test_container.id[:12], + loginuid=loginuid) + chown = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {fut}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + + events.extend([ + Event(process=touch, event_type=EventType.CREATION, file=fut, + host_path=''), + Event(process=chown, event_type=EventType.OWNERSHIP, file=fut, + host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), + ]) + + for e in events: + print(f'Waiting for event: {e}') + + server.wait_events(events) + + +def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): + """ + Tests that ownership events on ignored files are not captured by the + server. + + Args: + fact: Fixture for file activity (only required to be running). + test_container: A container for running commands in. + monitored_dir: Temporary directory path for creating the test file. + ignored_dir: Temporary directory path that is not monitored by fact. + server: The server instance to communicate with. + """ + loginuid = pow(2, 32) - 1 + + ignored_file = '/test.txt' + monitored_file = '/container-dir/test.txt' + + test_container.exec_run(f'touch {ignored_file}') + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {ignored_file}') + test_container.exec_run(f'touch {monitored_file}') + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {monitored_file}') + + ignored_touch = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/touch', + args=f'touch {ignored_file}', + name='touch', + container_id=test_container.id[:12], + loginuid=loginuid) + ignored_chown = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {ignored_file}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + reported_touch = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/touch', + args=f'touch {monitored_file}', + name='touch', + container_id=test_container.id[:12], + loginuid=loginuid) + reported_chown = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {monitored_file}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + + # events + ignored_create_event = Event(process=ignored_touch, + event_type=EventType.CREATION, + file=ignored_file, + host_path='') + reported_create_event = Event(process=reported_touch, + event_type=EventType.CREATION, + file=monitored_file, + host_path='') + ignored_chmod_event = Event(process=ignored_chown, + event_type=EventType.OWNERSHIP, + file=ignored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID) + reported_chmod_event = Event(process=reported_chown, + event_type=EventType.OWNERSHIP, + file=monitored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID) + + server.wait_events(events=[reported_create_event, reported_chmod_event], + ignored=[ignored_create_event, ignored_chmod_event]) + + +def test_no_change(fact, test_container, server): + """ + Tests that chown to the same UID/GID triggers events for all calls. + + Args: + fact: Fixture for file activity (only required to be running). + test_container: A container for running commands in. + server: The server instance to communicate with. + """ + # File Under Test + fut = '/container-dir/test.txt' + + # Create the file + test_container.exec_run(f'touch {fut}') + + # First chown to TEST_UID:TEST_GID - this should trigger an event + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + + # Second chown to the same UID/GID - this should ALSO trigger an event + test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + + loginuid = pow(2, 32) - 1 + touch = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/touch', + args=f'touch {fut}', + name='touch', + container_id=test_container.id[:12], + loginuid=loginuid) + chown1 = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {fut}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + chown2 = Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=f'chown {TEST_UID}:{TEST_GID} {fut}', + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + + # Expect both chown events (all calls to chown trigger events) + events = [ + Event(process=touch, event_type=EventType.CREATION, file=fut, + host_path=''), + Event(process=chown1, event_type=EventType.OWNERSHIP, file=fut, + host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), + Event(process=chown2, event_type=EventType.OWNERSHIP, file=fut, + host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), + ] + + for e in events: + print(f'Waiting for event: {e}') + + server.wait_events(events) + From b381565a8566d21207d06100968cbb11a0643e84 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:02:59 +0100 Subject: [PATCH 06/16] Explain why trace_path_chown takes unsigned long long --- fact-ebpf/src/bpf/main.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index f95e78a1..e564c433 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -174,6 +174,9 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { return 0; } +/* path_chown takes _unsigned long long_ for uid and gid because kuid_t and kgid_t (structs) + fit in registers and since they contain only one integer, their content is extended to the + size of the BPF registers (64 bits) to simplify further arithmetic operations. */ SEC("lsm/path_chown") int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsigned long long gid) { struct metrics_t* m = get_metrics(); From 22dcedbfb83c5941404ef223bbcf0b87cf0ff5e3 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:07:57 +0100 Subject: [PATCH 07/16] Simplify retrieving of the old uid/gid Co-authored-by: Mauro Ezequiel Moltrasio --- fact-ebpf/src/bpf/main.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index e564c433..7f4b2882 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -215,10 +215,8 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign } struct dentry* d = BPF_CORE_READ(path, dentry); - kuid_t kuid = BPF_CORE_READ(d, d_inode, i_uid); - kgid_t kgid = BPF_CORE_READ(d, d_inode, i_gid); - unsigned long long old_uid = BPF_CORE_READ(&kuid, val); - unsigned long long old_gid = BPF_CORE_READ(&kgid, val); + unsigned long long old_uid = BPF_CORE_READ(d, d_inode, i_uid.val); + unsigned long long old_gid = BPF_CORE_READ(d, d_inode, i_gid.val); submit_owner_event(&m->path_chown, bound_path->path, From 615b751295734f7e2a3d833ef1833d12c34559ee Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:11:24 +0100 Subject: [PATCH 08/16] Rename submit_owner_event() ... to submit_ownership_event() --- fact-ebpf/src/bpf/events.h | 16 ++++++++-------- fact-ebpf/src/bpf/main.c | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index a4c1225d..9cc6baae 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -74,14 +74,14 @@ __always_inline static void submit_mode_event(struct metrics_by_hook_t* m, __submit_event(event, m, FILE_ACTIVITY_CHMOD, filename, inode, use_bpf_d_path); } -__always_inline static void submit_owner_event(struct metrics_by_hook_t* m, - const char filename[PATH_MAX], - inode_key_t* inode, - unsigned long long uid, - unsigned long long gid, - unsigned long long old_uid, - unsigned long long old_gid, - bool use_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, + unsigned long long uid, + unsigned long long gid, + unsigned long long old_uid, + unsigned long long old_gid, + bool use_bpf_d_path) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); if (event == NULL) { m->ringbuffer_full++; diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 7f4b2882..3b448128 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -218,14 +218,14 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign unsigned long long old_uid = BPF_CORE_READ(d, d_inode, i_uid.val); unsigned long long old_gid = BPF_CORE_READ(d, d_inode, i_gid.val); - submit_owner_event(&m->path_chown, - bound_path->path, - &inode_key, - uid, - gid, - old_uid, - old_gid, - path_hooks_support_bpf_d_path); + submit_ownership_event(&m->path_chown, + bound_path->path, + &inode_key, + uid, + gid, + old_uid, + old_gid, + path_hooks_support_bpf_d_path); return 0; } From 678033e496c19e30bead6b1babe8f76a02f73ffb Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:15:37 +0100 Subject: [PATCH 09/16] Remove unused functions --- fact/src/event/mod.rs | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 089d98a3..c4d1da80 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -272,23 +272,6 @@ pub struct ChmodFileData { old_mode: u16, } -impl ChmodFileData { - pub fn new( - filename: [c_char; PATH_MAX as usize], - inode: inode_key_t, - new_mode: u16, - old_mode: u16, - ) -> anyhow::Result { - let inner = BaseFileData::new(filename, inode)?; - - Ok(ChmodFileData { - inner, - new_mode, - old_mode, - }) - } -} - impl From for fact_api::FilePermissionChange { fn from(value: ChmodFileData) -> Self { let ChmodFileData { @@ -322,26 +305,6 @@ pub struct ChownFileData { old_gid: u32, } -impl ChownFileData { - pub fn new( - filename: [c_char; PATH_MAX as usize], - inode: inode_key_t, - new_uid: u32, - new_gid: u32, - old_uid: u32, - old_gid: u32, - ) -> anyhow::Result { - let file = BaseFileData::new(filename, inode)?; - - Ok(ChownFileData { - inner: file, - new_uid, - new_gid, - old_uid, - old_gid, - }) - } -} impl From for fact_api::FileOwnershipChange { fn from(value: ChownFileData) -> Self { let ChownFileData { From f591a991290cfd58a4a7aca07be83e9cc479cda4 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:18:01 +0100 Subject: [PATCH 10/16] Use catch-all in struct affectation --- fact/src/event/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index c4d1da80..e43d8b58 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -277,7 +277,7 @@ impl From for fact_api::FilePermissionChange { let ChmodFileData { inner: file, new_mode, - old_mode: _, + .. } = value; let activity = fact_api::FileActivityBase::from(file); fact_api::FilePermissionChange { @@ -311,8 +311,7 @@ impl From for fact_api::FileOwnershipChange { inner: file, new_uid, new_gid, - old_uid: _, - old_gid: _, + .. } = value; let activity = fact_api::FileActivityBase::from(file); fact_api::FileOwnershipChange { From c3ca7c44a14bad5f7d46d25c73ec5f8a6784afcd Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:20:30 +0100 Subject: [PATCH 11/16] Add missing chown metric accumulation call --- fact-ebpf/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index 5ba66dc5..655d48d7 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -112,6 +112,7 @@ impl metrics_t { m.file_open = m.file_open.accumulate(&other.file_open); m.path_unlink = m.path_unlink.accumulate(&other.path_unlink); m.path_chmod = m.path_chmod.accumulate(&other.path_chmod); + m.path_chown = m.path_chown.accumulate(&other.path_chown); m } } From 0fb43a3f944075eeaa1b10c7b8d0cb8dc8e2c46c Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:47:11 +0100 Subject: [PATCH 12/16] Factorize the command strings --- tests/test_path_chown.py | 87 ++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index cf5d41d7..78e0daee 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -24,15 +24,17 @@ def test_chown(fact, test_container, server): fut = '/container-dir/test.txt' # Create the file and chown it - test_container.exec_run(f'touch {fut}') - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + touch_cmd = f'touch {fut}' + chown_cmd = f'chown {TEST_UID}:{TEST_GID} {fut}' + test_container.exec_run(touch_cmd) + test_container.exec_run(chown_cmd) loginuid = pow(2, 32) - 1 touch = Process(pid=None, uid=0, gid=0, exe_path='/usr/bin/touch', - args=f'touch {fut}', + args=touch_cmd, name='touch', container_id=test_container.id[:12], loginuid=loginuid) @@ -40,7 +42,7 @@ def test_chown(fact, test_container, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {fut}', + args=chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) @@ -73,14 +75,16 @@ def test_multiple(fact, test_container, server): # File Under Test for i in range(3): fut = f'/container-dir/{i}.txt' - test_container.exec_run(f'touch {fut}') - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + touch_cmd = f'touch {fut}' + chown_cmd = f'chown {TEST_UID}:{TEST_GID} {fut}' + test_container.exec_run(touch_cmd) + test_container.exec_run(chown_cmd) touch = Process(pid=None, uid=0, gid=0, exe_path='/usr/bin/touch', - args=f'touch {fut}', + args=touch_cmd, name='touch', container_id=test_container.id[:12], loginuid=loginuid) @@ -88,7 +92,7 @@ def test_multiple(fact, test_container, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {fut}', + args=chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) @@ -123,16 +127,21 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): ignored_file = '/test.txt' monitored_file = '/container-dir/test.txt' - test_container.exec_run(f'touch {ignored_file}') - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {ignored_file}') - test_container.exec_run(f'touch {monitored_file}') - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {monitored_file}') + ignored_touch_cmd = f'touch {ignored_file}' + ignored_chown_cmd = f'chown {TEST_UID}:{TEST_GID} {ignored_file}' + monitored_touch_cmd = f'touch {monitored_file}' + monitored_chown_cmd = f'chown {TEST_UID}:{TEST_GID} {monitored_file}' + + test_container.exec_run(ignored_touch_cmd) + test_container.exec_run(ignored_chown_cmd) + test_container.exec_run(monitored_touch_cmd) + test_container.exec_run(monitored_chown_cmd) ignored_touch = Process(pid=None, uid=0, gid=0, exe_path='/usr/bin/touch', - args=f'touch {ignored_file}', + args=ignored_touch_cmd, name='touch', container_id=test_container.id[:12], loginuid=loginuid) @@ -140,7 +149,7 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {ignored_file}', + args=ignored_chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) @@ -148,7 +157,7 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): uid=0, gid=0, exe_path='/usr/bin/touch', - args=f'touch {monitored_file}', + args=monitored_touch_cmd, name='touch', container_id=test_container.id[:12], loginuid=loginuid) @@ -156,12 +165,11 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {monitored_file}', + args=monitored_chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) - # events ignored_create_event = Event(process=ignored_touch, event_type=EventType.CREATION, file=ignored_file, @@ -170,21 +178,21 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): event_type=EventType.CREATION, file=monitored_file, host_path='') - ignored_chmod_event = Event(process=ignored_chown, - event_type=EventType.OWNERSHIP, - file=ignored_file, - host_path='', - owner_uid=TEST_UID, - owner_gid=TEST_GID) - reported_chmod_event = Event(process=reported_chown, - event_type=EventType.OWNERSHIP, - file=monitored_file, - host_path='', - owner_uid=TEST_UID, - owner_gid=TEST_GID) - - server.wait_events(events=[reported_create_event, reported_chmod_event], - ignored=[ignored_create_event, ignored_chmod_event]) + ignored_chown_event = Event(process=ignored_chown, + event_type=EventType.OWNERSHIP, + file=ignored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID) + reported_chown_event = Event(process=reported_chown, + event_type=EventType.OWNERSHIP, + file=monitored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID) + + server.wait_events(events=[reported_create_event, reported_chown_event], + ignored=[ignored_create_event, ignored_chown_event]) def test_no_change(fact, test_container, server): @@ -199,21 +207,24 @@ def test_no_change(fact, test_container, server): # File Under Test fut = '/container-dir/test.txt' + touch_cmd = f'touch {fut}' + chown_cmd = f'chown {TEST_UID}:{TEST_GID} {fut}' + # Create the file - test_container.exec_run(f'touch {fut}') + test_container.exec_run(touch_cmd) # First chown to TEST_UID:TEST_GID - this should trigger an event - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + test_container.exec_run(chown_cmd) # Second chown to the same UID/GID - this should ALSO trigger an event - test_container.exec_run(f'chown {TEST_UID}:{TEST_GID} {fut}') + test_container.exec_run(chown_cmd) loginuid = pow(2, 32) - 1 touch = Process(pid=None, uid=0, gid=0, exe_path='/usr/bin/touch', - args=f'touch {fut}', + args=touch_cmd, name='touch', container_id=test_container.id[:12], loginuid=loginuid) @@ -221,7 +232,7 @@ def test_no_change(fact, test_container, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {fut}', + args=chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) @@ -229,7 +240,7 @@ def test_no_change(fact, test_container, server): uid=0, gid=0, exe_path='/usr/bin/chown', - args=f'chown {TEST_UID}:{TEST_GID} {fut}', + args=chown_cmd, name='chown', container_id=test_container.id[:12], loginuid=loginuid) From 9091d76afd305f2c23a34690af6c3e839c5d8afd Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:52:51 +0100 Subject: [PATCH 13/16] Remove unused fixtures --- tests/test_path_chown.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 78e0daee..5c534e17 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -110,7 +110,7 @@ def test_multiple(fact, test_container, server): server.wait_events(events) -def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): +def test_ignored(fact, test_container, server): """ Tests that ownership events on ignored files are not captured by the server. @@ -118,8 +118,6 @@ def test_ignored(fact, test_container, monitored_dir, ignored_dir, server): Args: fact: Fixture for file activity (only required to be running). test_container: A container for running commands in. - monitored_dir: Temporary directory path for creating the test file. - ignored_dir: Temporary directory path that is not monitored by fact. server: The server instance to communicate with. """ loginuid = pow(2, 32) - 1 From 576e81d4f511637166a29ece5aa42f7abd944398 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 16:58:51 +0100 Subject: [PATCH 14/16] Enhance code readability Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_chown.py | 54 +++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 5c534e17..631c2847 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -167,30 +167,38 @@ def test_ignored(fact, test_container, server): name='chown', container_id=test_container.id[:12], loginuid=loginuid) + ignored_events = [ + Event(process=ignored_touch, + event_type=EventType.CREATION, + file=ignored_file, + host_path=''), + Event(process=ignored_chown, + event_type=EventType.OWNERSHIP, + file=ignored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID), + ] + expected_events = [ + Event(process=reported_touch, + event_type=EventType.CREATION, + file=monitored_file, + host_path=''), + Event(process=reported_chown, + event_type=EventType.OWNERSHIP, + file=monitored_file, + host_path='', + owner_uid=TEST_UID, + owner_gid=TEST_GID), + ] + + for e in ignored_events: + print(f'Events that should be ignored: {e}') + + for e in expected_events: + print(f'Waiting for event: {e}') - ignored_create_event = Event(process=ignored_touch, - event_type=EventType.CREATION, - file=ignored_file, - host_path='') - reported_create_event = Event(process=reported_touch, - event_type=EventType.CREATION, - file=monitored_file, - host_path='') - ignored_chown_event = Event(process=ignored_chown, - event_type=EventType.OWNERSHIP, - file=ignored_file, - host_path='', - owner_uid=TEST_UID, - owner_gid=TEST_GID) - reported_chown_event = Event(process=reported_chown, - event_type=EventType.OWNERSHIP, - file=monitored_file, - host_path='', - owner_uid=TEST_UID, - owner_gid=TEST_GID) - - server.wait_events(events=[reported_create_event, reported_chown_event], - ignored=[ignored_create_event, ignored_chown_event]) + server.wait_events(events=expected_events, ignored=ignored_events) def test_no_change(fact, test_container, server): From 977483084361f517c740be7bed942697d96c94b1 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 17:16:27 +0100 Subject: [PATCH 15/16] Rework process definition Co-authored-by: Mauro Ezequiel Moltrasio --- tests/test_path_chown.py | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index 631c2847..d318f4eb 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -234,31 +234,31 @@ def test_no_change(fact, test_container, server): name='touch', container_id=test_container.id[:12], loginuid=loginuid) - chown1 = Process(pid=None, - uid=0, - gid=0, - exe_path='/usr/bin/chown', - args=chown_cmd, - name='chown', - container_id=test_container.id[:12], - loginuid=loginuid) - chown2 = Process(pid=None, - uid=0, - gid=0, - exe_path='/usr/bin/chown', - args=chown_cmd, - name='chown', - container_id=test_container.id[:12], - loginuid=loginuid) - + chown = [ + Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=chown_cmd, + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid), + Process(pid=None, + uid=0, + gid=0, + exe_path='/usr/bin/chown', + args=chown_cmd, + name='chown', + container_id=test_container.id[:12], + loginuid=loginuid) + ] + # Expect both chown events (all calls to chown trigger events) events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=chown1, event_type=EventType.OWNERSHIP, file=fut, - host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), - Event(process=chown2, event_type=EventType.OWNERSHIP, file=fut, - host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), + *(Event(process=p, event_type=EventType.OWNERSHIP, file=fut, + host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID) for p in chown), ] for e in events: From fc392f4d39ff064e963eb5422481b73e34ff72ae Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Thu, 15 Jan 2026 17:21:51 +0100 Subject: [PATCH 16/16] Make uid and gid mandatory in chown events Co-authored-by: Mauro Ezequiel Moltrasio --- tests/event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/event.py b/tests/event.py index ce7b524a..389c01e0 100644 --- a/tests/event.py +++ b/tests/event.py @@ -233,8 +233,8 @@ def __eq__(self, other: Any) -> bool: elif self.event_type == EventType.OWNERSHIP: return self.file == other.ownership.activity.path and \ self.host_path == other.ownership.activity.host_path and \ - (self.owner_uid is None or self.owner_uid == other.ownership.uid) and \ - (self.owner_gid is None or self.owner_gid == other.ownership.gid) + self.owner_uid == other.ownership.uid and \ + self.owner_gid == other.ownership.gid return False raise NotImplementedError