diff --git a/fact-ebpf/src/bpf/bound_path.h b/fact-ebpf/src/bpf/bound_path.h index 0cbe7ebb..80024050 100644 --- a/fact-ebpf/src/bpf/bound_path.h +++ b/fact-ebpf/src/bpf/bound_path.h @@ -19,8 +19,8 @@ __always_inline static void path_write_char(char* p, unsigned int offset, char c *path_safe_access(p, offset) = c; } -__always_inline static struct bound_path_t* _path_read(struct path* path, bool use_bpf_d_path) { - struct bound_path_t* bound_path = get_bound_path(); +__always_inline static struct bound_path_t* _path_read(struct path* path, bound_path_buffer_t key, bool use_bpf_d_path) { + struct bound_path_t* bound_path = get_bound_path(key); if (bound_path == NULL) { return NULL; } @@ -37,11 +37,19 @@ __always_inline static struct bound_path_t* _path_read(struct path* path, bool u } __always_inline static struct bound_path_t* path_read(struct path* path) { - return _path_read(path, true); + return _path_read(path, BOUND_PATH_MAIN, true); } __always_inline static struct bound_path_t* path_read_no_d_path(struct path* path) { - return _path_read(path, false); + return _path_read(path, BOUND_PATH_MAIN, false); +} + +__always_inline static struct bound_path_t* path_read_alt(struct path* path) { + return _path_read(path, BOUND_PATH_ALTERNATE, true); +} + +__always_inline static struct bound_path_t* path_read_alt_no_d_path(struct path* path) { + return _path_read(path, BOUND_PATH_ALTERNATE, false); } enum path_append_status_t { @@ -69,3 +77,47 @@ __always_inline static enum path_append_status_t path_append_dentry(struct bound return 0; } + +__always_inline static struct bound_path_t* _path_read_append_d_entry(struct path* dir, struct dentry* dentry, bound_path_buffer_t key) { + struct bound_path_t* path = _path_read(dir, key, path_hooks_support_bpf_d_path); + + if (path == NULL) { + bpf_printk("Failed to read path"); + return NULL; + } + path_write_char(path->path, path->len - 1, '/'); + + switch (path_append_dentry(path, dentry)) { + case PATH_APPEND_SUCCESS: + break; + case PATH_APPEND_INVALID_LENGTH: + bpf_printk("Invalid path length: %u", path->len); + return NULL; + case PATH_APPEND_READ_ERROR: + bpf_printk("Failed to read final path component"); + return NULL; + } + return path; +} + +/** + * Read the path and append the supplied dentry. + * + * A very common pattern in the kernel is to provide a struct path to a + * directory and a dentry to an element in said directory, this helper + * provides a short way of resolving the full path in one call. + */ +__always_inline static struct bound_path_t* path_read_append_d_entry(struct path* dir, struct dentry* dentry) { + return _path_read_append_d_entry(dir, dentry, BOUND_PATH_MAIN); +} + +/** + * Read the path and append the supplied dentry. + * + * This works essentially the same as path_read_append_d_entry, but does + * so in an alternate buffer. Useful for operations that take more than + * one path, like path_rename. + */ +__always_inline static struct bound_path_t* path_read_alt_append_d_entry(struct path* dir, struct dentry* dentry) { + return _path_read_append_d_entry(dir, dentry, BOUND_PATH_ALTERNATE); +} diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 9cc6baae..be1237c6 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -95,3 +95,21 @@ __always_inline static void submit_ownership_event(struct metrics_by_hook_t* m, __submit_event(event, m, FILE_ACTIVITY_CHOWN, filename, inode, use_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, + 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; + } + + 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, use_bpf_d_path); +} diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index 842f1251..4e9a26dc 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -85,7 +85,7 @@ typedef enum inode_monitored_t { * 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* inode) { +__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* volatile inode) { if (inode != NULL) { return MONITORED; } diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 3b448128..39a0622f 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -74,28 +74,11 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { m->path_unlink.total++; - struct bound_path_t* path = NULL; - if (path_hooks_support_bpf_d_path) { - path = path_read(dir); - } else { - path = path_read_no_d_path(dir); - } - + struct bound_path_t* path = path_read_append_d_entry(dir, dentry); if (path == NULL) { bpf_printk("Failed to read path"); - goto error; - } - path_write_char(path->path, path->len - 1, '/'); - - switch (path_append_dentry(path, dentry)) { - case PATH_APPEND_SUCCESS: - break; - case PATH_APPEND_INVALID_LENGTH: - bpf_printk("Invalid path length: %u", path->len); - goto error; - case PATH_APPEND_READ_ERROR: - bpf_printk("Failed to read final path component"); - goto error; + m->path_unlink.error++; + return 0; } inode_key_t inode_key = inode_to_key(dentry->d_inode); @@ -120,10 +103,6 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { &inode_key, path_hooks_support_bpf_d_path); return 0; - -error: - m->path_unlink.error++; - return 0; } SEC("lsm/path_chmod") @@ -229,3 +208,52 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign return 0; } + +SEC("lsm/path_rename") +int BPF_PROG(trace_path_rename, struct path* old_dir, + struct dentry* old_dentry, struct path* new_dir, + struct dentry* new_dentry, unsigned int flags) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + + m->path_rename.total++; + + struct bound_path_t* new_path = path_read_append_d_entry(new_dir, new_dentry); + if (new_path == NULL) { + bpf_printk("Failed to read path"); + goto error; + } + + struct bound_path_t* old_path = path_read_alt_append_d_entry(old_dir, old_dentry); + if (old_path == NULL) { + bpf_printk("Failed to read path"); + goto error; + } + + inode_key_t old_inode = inode_to_key(old_dentry->d_inode); + const inode_value_t* volatile old_inode_value = inode_get(&old_inode); + inode_key_t new_inode = inode_to_key(new_dentry->d_inode); + const inode_value_t* volatile new_inode_value = inode_get(&new_inode); + + if (inode_is_monitored(old_inode_value) == NOT_MONITORED && + inode_is_monitored(new_inode_value) == NOT_MONITORED && + !is_monitored(old_path) && + !is_monitored(new_path)) { + m->path_rename.ignored++; + return 0; + } + + submit_rename_event(&m->path_rename, + new_path->path, + old_path->path, + &old_inode, + &new_inode, + path_hooks_support_bpf_d_path); + return 0; + +error: + m->path_rename.error++; + return 0; +} diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index 37849f2d..13f5fa5d 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -79,12 +79,16 @@ struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __type(key, __u32); __type(value, struct bound_path_t); - __uint(max_entries, 1); + __uint(max_entries, 2); } bound_path_heap SEC(".maps"); -__always_inline static struct bound_path_t* get_bound_path() { - unsigned int zero = 0; - return bpf_map_lookup_elem(&bound_path_heap, &zero); +typedef enum { + BOUND_PATH_MAIN = 0, + BOUND_PATH_ALTERNATE = 1, +} bound_path_buffer_t; + +__always_inline static struct bound_path_t* get_bound_path(bound_path_buffer_t key) { + return bpf_map_lookup_elem(&bound_path_heap, &key); } struct { diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 8fce112e..6a009e66 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -54,6 +54,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_UNLINK, FILE_ACTIVITY_CHMOD, FILE_ACTIVITY_CHOWN, + FILE_ACTIVITY_RENAME, } file_activity_type_t; struct event_t { @@ -73,6 +74,10 @@ struct event_t { unsigned int gid; } old, new; } chown; + struct { + char old_filename[PATH_MAX]; + inode_key_t old_inode; + } rename; }; }; @@ -104,4 +109,5 @@ struct metrics_t { struct metrics_by_hook_t path_unlink; struct metrics_by_hook_t path_chmod; struct metrics_by_hook_t path_chown; + struct metrics_by_hook_t path_rename; }; diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index 655d48d7..0bf7a1ad 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -113,6 +113,7 @@ impl metrics_t { 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.path_rename = m.path_rename.accumulate(&other.path_rename); m } } diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index ec9ca57f..71077cb8 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -269,7 +269,7 @@ mod bpf_tests { tokio::time::sleep(Duration::from_millis(500)).await; // Create a file - let file = NamedTempFile::new_in(monitored_path).expect("Failed to create temporary file"); + let file = NamedTempFile::new_in(&monitored_path).expect("Failed to create temporary file"); println!("Created {file:?}"); // Trigger permission changes @@ -286,6 +286,12 @@ mod bpf_tests { let current = Process::current(); let file_path = file.path().to_path_buf(); + // Trigger a file rename + let renamed_path = monitored_path.join("target"); + std::fs::rename(&file, &renamed_path).expect("Failed to rename file"); + // Move the file back so it can be properly closed + std::fs::rename(&renamed_path, &file).expect("Failed to rename file"); + let expected_events = [ Event::new( EventTestData::Creation, @@ -303,6 +309,22 @@ mod bpf_tests { current.clone(), ) .unwrap(), + Event::new( + EventTestData::Rename(file_path.clone()), + host_info::get_hostname(), + renamed_path.clone(), + PathBuf::new(), + current.clone(), + ) + .unwrap(), + Event::new( + EventTestData::Rename(renamed_path), + host_info::get_hostname(), + file_path.clone(), + PathBuf::new(), + current.clone(), + ) + .unwrap(), Event::new( EventTestData::Unlink, host_info::get_hostname(), @@ -312,7 +334,6 @@ mod bpf_tests { ) .unwrap(), ]; - // Close the file, removing it file.close().expect("Failed to close temp file"); diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 463fb2c7..c85189d5 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -64,6 +64,7 @@ pub(crate) enum EventTestData { Creation, Unlink, Chmod(u16, u16), + Rename(PathBuf), } #[derive(Debug, Clone, Serialize)] @@ -103,6 +104,16 @@ impl Event { }; FileData::Chmod(data) } + EventTestData::Rename(old_path) => { + let data = RenameFileData { + new: inner, + old: BaseFileData { + filename: old_path, + ..Default::default() + }, + }; + FileData::Rename(data) + } }; Ok(Event { @@ -113,6 +124,11 @@ impl Event { }) } + /// Unwrap the inner FileData and return the inode that triggered + /// the event. + /// + /// In the case of operations that involve two inodes, like rename, + /// the 'new' inode will be returned. pub fn get_inode(&self) -> &inode_key_t { match &self.file { FileData::Open(data) => &data.inode, @@ -120,9 +136,24 @@ impl Event { FileData::Unlink(data) => &data.inode, FileData::Chmod(data) => &data.inner.inode, FileData::Chown(data) => &data.inner.inode, + FileData::Rename(data) => &data.new.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. + pub fn get_old_inode(&self) -> Option<&inode_key_t> { + match &self.file { + FileData::Rename(data) => Some(&data.old.inode), + _ => None, + } + } + + /// Set the `host_file` field of the event to the one provided. + /// + /// In the case of operations that involve two paths, like rename, + /// the 'new' host_file will be set. pub fn set_host_path(&mut self, host_path: PathBuf) { match &mut self.file { FileData::Open(data) => data.host_file = host_path, @@ -130,6 +161,15 @@ impl Event { 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, + FileData::Rename(data) => data.new.host_file = host_path, + } + } + + /// Same as `set_host_path` but setting the 'old' host_file for + /// operations that have one, like rename. + pub fn set_old_host_path(&mut self, host_path: PathBuf) { + if let FileData::Rename(data) = &mut self.file { + data.old.host_file = host_path } } } @@ -185,6 +225,7 @@ pub enum FileData { Unlink(BaseFileData), Chmod(ChmodFileData), Chown(ChownFileData), + Rename(RenameFileData), } impl FileData { @@ -217,6 +258,15 @@ impl FileData { }; FileData::Chown(data) } + file_activity_type_t::FILE_ACTIVITY_RENAME => { + let old_filename = unsafe { extra_data.rename.old_filename }; + let old_inode = unsafe { extra_data.rename.old_inode }; + let data = RenameFileData { + new: inner, + old: BaseFileData::new(old_filename, old_inode)?, + }; + FileData::Rename(data) + } invalid => unreachable!("Invalid event type: {invalid:?}"), }; @@ -250,6 +300,10 @@ impl From for fact_api::file_activity::File { let f_act = fact_api::FileOwnershipChange::from(event); fact_api::file_activity::File::Ownership(f_act) } + FileData::Rename(event) => { + let f_act = fact_api::FileRename::from(event); + fact_api::file_activity::File::Rename(f_act) + } } } } @@ -262,12 +316,13 @@ impl PartialEq for FileData { (FileData::Creation(this), FileData::Creation(other)) => this == other, (FileData::Unlink(this), FileData::Unlink(other)) => this == other, (FileData::Chmod(this), FileData::Chmod(other)) => this == other, + (FileData::Rename(this), FileData::Rename(other)) => this == other, _ => false, } } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Default)] pub struct BaseFileData { pub filename: PathBuf, host_file: PathBuf, @@ -359,6 +414,30 @@ impl From for fact_api::FileOwnershipChange { } } +#[derive(Debug, Clone, Serialize)] +pub struct RenameFileData { + new: BaseFileData, + old: BaseFileData, +} + +impl From for fact_api::FileRename { + fn from(RenameFileData { new, old }: RenameFileData) -> Self { + let new = fact_api::FileActivityBase::from(new); + let old = fact_api::FileActivityBase::from(old); + fact_api::FileRename { + old: Some(old), + new: Some(new), + } + } +} + +#[cfg(test)] +impl PartialEq for RenameFileData { + fn eq(&self, other: &Self) -> bool { + self.new == other.new && self.old == other.old + } +} + #[cfg(test)] mod test_utils { use std::os::raw::c_char; diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index ac2b5bae..2adbb58d 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -126,10 +126,10 @@ impl HostScanner { self.tx.subscribe() } - fn get_host_path(&self, inode: &inode_key_t) -> Option { + fn get_host_path(&self, inode: Option<&inode_key_t>) -> Option { // The path here needs to be cloned because we won't keep the // inode_map borrow long enough. - self.inode_map.borrow().get(inode).cloned() + self.inode_map.borrow().get(inode?).cloned() } pub fn start(mut self) -> JoinHandle> { @@ -144,10 +144,14 @@ impl HostScanner { break; }; - if let Some(host_path) = self.get_host_path(event.get_inode()) { + if let Some(host_path) = self.get_host_path(Some(event.get_inode())) { event.set_host_path(host_path); } + if let Some(host_path) = self.get_host_path(event.get_old_inode()) { + event.set_old_host_path(host_path); + } + let event = Arc::new(event); if let Err(e) = self.tx.send(event) { warn!("Failed to send event: {e}"); diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 1a6fc977..d1a3a242 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -12,6 +12,7 @@ pub struct KernelMetrics { path_unlink: EventCounter, path_chmod: EventCounter, path_chown: EventCounter, + path_rename: EventCounter, map: PerCpuArray, } @@ -37,17 +38,24 @@ impl KernelMetrics { "Events processed by the path_chown LSM hook", &[], // Labels are not needed since `collect` will add them all ); + let path_rename = EventCounter::new( + "kernel_path_rename_events", + "Events processed by the path_rename LSM hook", + &[], // Labels are not needed since `collect` will add them all + ); file_open.register(reg); path_unlink.register(reg); path_chmod.register(reg); path_chown.register(reg); + path_rename.register(reg); KernelMetrics { file_open, path_unlink, path_chmod, path_chown, + path_rename, map: kernel_metrics, } } @@ -96,6 +104,7 @@ impl KernelMetrics { 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); + KernelMetrics::refresh_labels(&self.path_rename, &metrics.path_rename); Ok(()) } diff --git a/tests/conftest.py b/tests/conftest.py index aee04534..143167bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ 'test_editors.commons' ] + def join_path_with_filename(directory, filename): """ Join a directory path with a filename, handling bytes filenames properly. @@ -54,6 +55,7 @@ def path_to_string(path): else: return path + @pytest.fixture def monitored_dir(): """ @@ -249,8 +251,8 @@ def fact(request, docker_client, fact_config, server, logs_dir, test_file): with open(metric_log, 'w') as f: f.write(resp.text) - container.stop(timeout=1) - exit_status = container.wait(timeout=1) + container.stop(timeout=2) + exit_status = container.wait(timeout=2) dump_logs(container, container_log) container.remove() assert exit_status['StatusCode'] == 0 diff --git a/tests/event.py b/tests/event.py index 7fee2cb8..324e2ec3 100644 --- a/tests/event.py +++ b/tests/event.py @@ -33,6 +33,7 @@ class EventType(Enum): UNLINK = 3 PERMISSION = 4 OWNERSHIP = 5 + RENAME = 6 class Process: @@ -190,14 +191,18 @@ class Event: event type and a file. """ - def __init__(self, - process: Process, - event_type: EventType, - file: str | Pattern[str], - host_path: str | Pattern[str] = '', - mode: int | None = None, - owner_uid: int | None = None, - owner_gid: int | None = None,): + def __init__( + self, + process: Process, + event_type: EventType, + file: str | Pattern[str], + host_path: str | Pattern[str] = '', + mode: int | None = None, + owner_uid: int | None = None, + owner_gid: int | None = None, + old_file: str | Pattern[str] | None = None, + old_host_path: str | Pattern[str] | None = None, + ): self._type: EventType = event_type self._process: Process = process self._file: str | Pattern[str] = file @@ -205,6 +210,8 @@ def __init__(self, self._mode: int | None = mode self._owner_uid: int | None = owner_uid self._owner_gid: int | None = owner_gid + self._old_file: str | Pattern[str] | None = old_file + self._old_host_path: str | Pattern[str] | None = old_host_path @property def event_type(self) -> EventType: @@ -234,6 +241,14 @@ def owner_uid(self) -> int | None: def owner_gid(self) -> int | None: return self._owner_gid + @property + def old_file(self) -> str | Pattern[str] | None: + return self._old_file + + @property + def old_host_path(self) -> str | Pattern[str] | None: + return self._old_host_path + @override def __eq__(self, other: Any) -> bool: if isinstance(other, FileActivity): @@ -258,6 +273,12 @@ def __eq__(self, other: Any) -> bool: cmp_path(self.host_path, other.ownership.activity.host_path) and \ self.owner_uid == other.ownership.uid and \ self.owner_gid == other.ownership.gid + elif self.event_type == EventType.RENAME: + return cmp_path(self.file, other.rename.new.path) and \ + cmp_path(self.host_path, other.rename.new.host_path) and \ + cmp_path(self.old_file, other.rename.old.path) and \ + cmp_path(self.old_host_path, other.rename.old.host_path) + return False raise NotImplementedError @@ -273,6 +294,9 @@ def __str__(self) -> str: if self.event_type == EventType.OWNERSHIP: s += f', owner=(uid={self.owner_uid}, gid={self.owner_gid})' + if self.event_type == EventType.RENAME: + s += f', old_file="{self.old_file}", old_host_path="{self.old_host_path}"' + s += ')' return s diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 32175d42..75eeac17 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -25,6 +25,7 @@ def test_new_file(editor_container, server): def test_open_file(editor_container, server): fut = '/mounted/test.txt' + fut_backup = f'{fut}~' container_id = editor_container.id[:12] # We ensure the file exists before editing. @@ -56,12 +57,14 @@ def test_open_file(editor_container, server): 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=''), + Event(process=nvim, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=nvim, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=nvim, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=nvim, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + file=fut_backup, host_path=''), ] server.wait_events(events, strict=True) @@ -91,6 +94,7 @@ def test_new_file_ovfs(editor_container, server): def test_open_file_ovfs(editor_container, server): fut = '/container-dir/test.txt' + fut_backup = f'{fut}~' container_id = editor_container.id[:12] # We ensure the file exists before editing. @@ -126,6 +130,8 @@ def test_open_file_ovfs(editor_container, server): 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=''), + Event(process=nvim, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=nvim, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=nvim, event_type=EventType.OPEN, @@ -133,7 +139,7 @@ def test_open_file_ovfs(editor_container, server): Event(process=nvim, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=nvim, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + 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 99accb88..39fd5d32 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -32,6 +32,8 @@ def test_sed(vi_container, server): file=sed_tmp_file, host_path=''), Event(process=sed, event_type=EventType.OWNERSHIP, file=sed_tmp_file, host_path='', 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=''), ] server.wait_events(events, strict=True) @@ -71,6 +73,8 @@ def test_sed_ovfs(vi_container, server): file=sed_tmp_file, host_path=''), Event(process=sed, event_type=EventType.OWNERSHIP, file=sed_tmp_file, host_path='', 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=''), ] server.wait_events(events, strict=True) diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index 65c4ef06..dd4b3c2b 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -84,6 +84,7 @@ def test_new_file_ovfs(vi_container, server): def test_open_file(vi_container, server): fut = '/mounted/test.txt' + fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') @@ -129,12 +130,14 @@ def test_open_file(vi_container, server): 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=''), + Event(process=vi_process, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] @@ -144,6 +147,7 @@ def test_open_file(vi_container, server): def test_open_file_ovfs(vi_container, server): fut = '/container-dir/test.txt' + fut_backup = f'{fut}~' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' vi_test_file = get_vi_test_file('/container-dir') @@ -199,6 +203,8 @@ def test_open_file_ovfs(vi_container, server): 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=''), + Event(process=vi_process, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=vi_process, event_type=EventType.OPEN, @@ -206,7 +212,7 @@ def test_open_file_ovfs(vi_container, server): Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 8919362b..5fa6bf92 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -80,6 +80,7 @@ def test_new_file_ovfs(editor_container, server): def test_open_file(editor_container, server): fut = '/mounted/test.txt' + fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') @@ -124,12 +125,14 @@ def test_open_file(editor_container, server): 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=''), + Event(process=vi_process, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] @@ -139,6 +142,7 @@ def test_open_file(editor_container, server): def test_open_file_ovfs(editor_container, server): fut = '/container-dir/test.txt' + fut_backup = f'{fut}~' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' vi_test_file = get_vi_test_file('/container-dir') @@ -193,6 +197,8 @@ def test_open_file_ovfs(editor_container, server): 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=''), + Event(process=vi_process, event_type=EventType.RENAME, + file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), Event(process=vi_process, event_type=EventType.OPEN, @@ -200,7 +206,7 @@ def test_open_file_ovfs(editor_container, server): Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, - file=f'{fut}~', host_path=''), + file=fut_backup, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] diff --git a/tests/test_path_rename.py b/tests/test_path_rename.py new file mode 100644 index 00000000..5d6eb42d --- /dev/null +++ b/tests/test_path_rename.py @@ -0,0 +1,198 @@ +import os + +import pytest + +from conftest import join_path_with_filename, path_to_string +from event import Event, EventType, Process + + +@pytest.mark.parametrize("filename", [ + pytest.param('rename.txt', id='ASCII'), + pytest.param('café.txt', id='French'), + pytest.param('файл.txt', id='Cyrillic'), + pytest.param('测试.txt', id='Chinese'), + pytest.param('🚀rocket.txt', id='Emoji'), + pytest.param(b'test\xff\xfe.txt', id='Invalid'), +]) +def test_rename(monitored_dir, server, filename): + """ + Tests the renaming of a file and verifies that the corresponding + events are captured by the server. + + Args: + monitored_dir: Temporary directory path for creating the test file. + server: The server instance to communicate with. + filename: Name of the target path to rename to. + """ + # File Under Test + fut = join_path_with_filename(monitored_dir, filename) + old_fut = os.path.join(monitored_dir, 'file.txt') + + # Create a new file, it will have an empty host_path. + with open(old_fut, 'w') as f: + f.write('This is a test') + os.rename(old_fut, fut) + os.rename(fut, old_fut) + + # Convert fut to string for the Event, replacing invalid UTF-8 with U+FFFD + fut = path_to_string(fut) + + events = [ + Event(process=Process.from_proc(), event_type=EventType.CREATION, + file=old_fut, host_path=''), + Event(process=Process.from_proc(), event_type=EventType.RENAME, + file=fut, host_path='', 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=''), + ] + + server.wait_events(events) + + +def test_ignored(monitored_dir, ignored_dir, server): + """ + Tests that rename events on ignored files are not captured by the + server. + + Args: + 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. + """ + ignored_path = os.path.join(ignored_dir, 'test.txt') + + with open(ignored_path, 'w') as f: + f.write('This is to be ignored') + new_ignored_path = os.path.join(ignored_dir, 'rename.txt') + + # Renaming in between ignored paths should not generate events + os.rename(ignored_path, new_ignored_path) + + # Renaming to a monitored path generates an event + new_path = os.path.join(monitored_dir, 'rename.txt') + os.rename(new_ignored_path, new_path) + + # Renaming from a monitored path generates an event too + os.rename(new_path, ignored_path) + + p = Process.from_proc() + events = [ + Event(process=p, event_type=EventType.RENAME, + file=new_path, host_path='', old_file=new_ignored_path, old_host_path=''), + Event(process=p, event_type=EventType.RENAME, + file=ignored_path, host_path='', old_file=new_path, old_host_path=''), + ] + + server.wait_events(events) + + +def test_rename_dir(monitored_dir, ignored_dir, server): + """ + Test renaming a directory is caught + + We start by creating a subdirectory in an ignored path and give it a + few files to test we only get events for the directory itself. We + then check renaming within the ignored path doesn't trigger events, + move the subdirectory to a monitored path, renaming it within that + path and moving it back out, these three interactions should + generate events for the directory only. + + Args: + 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. + """ + # Directory Under Test + dut = os.path.join(monitored_dir, 'some-dir') + new_dut = os.path.join(monitored_dir, 'other-dir') + ignored_dut = os.path.join(ignored_dir, 'some-dir') + new_ignored_dut = os.path.join(ignored_dir, 'other-dir') + + os.mkdir(ignored_dut) + for i in range(3): + with open(os.path.join(ignored_dut, f'{i}.txt'), 'w') as f: + f.write('This is a test') + + # This rename should generate no events + os.rename(ignored_dut, new_ignored_dut) + + # The next three renames need to generate one event each + os.rename(new_ignored_dut, dut) + os.rename(dut, new_dut) + os.rename(new_dut, ignored_dut) + + p = Process.from_proc() + events = [ + Event(process=p, event_type=EventType.RENAME, file=dut, + host_path='', old_file=new_ignored_dut, old_host_path=''), + Event(process=p, event_type=EventType.RENAME, + file=new_dut, host_path='', old_file=dut, old_host_path=''), + Event(process=p, event_type=EventType.RENAME, + file=ignored_dut, host_path='', old_file=new_dut, old_host_path=''), + ] + + server.wait_events(events) + + +def test_overlay(test_container, server): + # File Under Test + fut = '/container-dir/test.txt' + new_fut = '/container-dir/rename.txt' + + # Create the exec and an equivalent event that it will trigger + test_container.exec_run(f'touch {fut}') + test_container.exec_run(f'mv {fut} {new_fut}') + + touch = Process.in_container( + exe_path='/usr/bin/touch', + args=f'touch {fut}', + name='touch', + container_id=test_container.id[:12], + ) + mv = Process.in_container( + exe_path='/usr/bin/mv', + args=f'mv {fut} {new_fut}', + name='mv', + container_id=test_container.id[:12], + ) + events = [ + Event(process=touch, event_type=EventType.CREATION, + file=fut, host_path=''), + Event(process=touch, event_type=EventType.OPEN, + file=fut, host_path=''), + Event(process=mv, event_type=EventType.RENAME, + file=new_fut, host_path='', old_file=fut, old_host_path=''), + ] + + server.wait_events(events) + + +def test_mounted_dir(test_container, ignored_dir, server): + # File Under Test + fut = '/mounted/test.txt' + new_fut = '/mounted/rename.txt' + + # Create the exec and an equivalent event that it will trigger + test_container.exec_run(f'touch {fut}') + test_container.exec_run(f'mv {fut} {new_fut}') + + touch = Process.in_container( + exe_path='/usr/bin/touch', + args=f'touch {fut}', + name='touch', + container_id=test_container.id[:12], + ) + mv = Process.in_container( + exe_path='/usr/bin/mv', + args=f'mv {fut} {new_fut}', + name='mv', + container_id=test_container.id[:12], + ) + events = [ + Event(process=touch, event_type=EventType.CREATION, + file=fut, host_path=''), + Event(process=mv, event_type=EventType.RENAME, + file=new_fut, host_path='', old_file=fut, old_host_path=''), + ] + + server.wait_events(events)