diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 914225d9a..024bd317e 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -720,9 +720,10 @@ The **file.is_file** method checks if a path exists and is a file. If it doesn't ### file.list -`file.list(path: str) -> List` +`file.list(path: str, dir_self: Optional = False) -> List` The **file.list** method returns a list of files at the specified path. The path is relative to your current working directory and can be traversed with `../`. +If `path` is a directory, `dir_self` denotes whether to show the information of `path` itself in the `List`. This field has no impact if `path` is not a directory. This function also supports globbing with `*` for example: ```python @@ -731,41 +732,65 @@ file.list("/etc/*ssh*") # List the contents of all dirs that have `ssh` in the n file.list("\\\\127.0.0.1\\c$\\Windows\\*.yml") # List files over UNC paths ``` -Each file is represented by a Dict type. -Here is an example of the Dict layout: +Here is a code snippet example, along with its output. +Each file is returned as a Dict with their respective information. + +```python +print(file.list("/tmp/some_dir", dir_self=True)) +``` + +**NOTE:** On systems without a specific time field being tracked, the field is ommitted. This means, for example, unix systems with `noatime` set will not have an `accessed` field visible in the `times` sub-Dict. ```json [ - { - "file_name": "implants", - "absolute_path": "/workspace/realm/implants", - "size": 4096, - "owner": "root", - "group": "0", - "permissions": "40755", - "modified": "2023-07-09 01:35:40 UTC", - "type": "Directory" + { + "absolute_path": "/tmp/some_dir", // if dir_self were set to False or unset, this Dict would not be present here + "file_name": "some_dir", + "group": "root", + "modified": "2026-07-12 18:17:39 UTC", + "owner": "root", + "permissions": "40775", + "size": 80, + "times": { + "accessed": 1783880431, + "changed": 1783880259, // changed is Unix-only (ctime) + "created": 1783880259, + "modified": 1783880259 }, - { - "file_name": "README.md", - "absolute_path": "/workspace/realm/README.md", - "size": 750, - "owner": "root", - "group": "0", - "permissions": "100644", - "modified": "2023-07-08 02:49:47 UTC", - "type": "File" + "type": "dir" + }, + { + "absolute_path": "/tmp/some_dir/some_file", + "file_name": "some_file", + "group": "root", + "modified": "2026-07-12 18:17:39 UTC", + "owner": "root", + "permissions": "100664", + "size": 5, + "times": { + "accessed": -2208988800, // negative epoch, represents 2208988800 seconds before Jan 1 1970 + "changed": 1783880431, + "created": 1783880259, + "modified": -2208988800 }, - { - "file_name": ".git", - "absolute_path": "/workspace/realm/.git", - "size": 4096, - "owner": "root", - "group": "0", - "permissions": "40755", - "modified": "2023-07-10 21:14:06 UTC", - "type": "Directory" - } + "type": "file" + }, + { + "absolute_path": "/tmp/some_dir/some_other_dir", + "file_name": "some_other_dir", + "group": "root", + "modified": "1900-01-01 00:00:00 UTC", + "owner": "root", + "permissions": "40775", + "size": 40, + "times": { + "accessed": 1783880259, + "changed": 1783880259, + "created": 1783880259, + "modified": 1783880259 + }, + "type": "dir" + } ] ``` diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs index 2dc411480..024fdf61e 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs @@ -172,7 +172,11 @@ impl FileLibrary for FileLibraryFake { } } - fn list(&self, path: Option) -> Result>, String> { + fn list( + &self, + path: Option, + dir_self: Option, + ) -> Result>, String> { let path = path.unwrap_or_else(|| "/".to_string()); let mut root = self.root.lock(); let parts = Self::normalize_path(&path); @@ -486,7 +490,7 @@ mod tests { assert_eq!(file.read("/tmp/test.txt".into()).unwrap(), "hello"); // List - let items = file.list(Some("/home/user".into())).unwrap(); + let items = file.list(Some("/home/user".into()), None).unwrap(); assert!( items .iter() diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs index e5c69525c..a8eed4179 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs @@ -142,6 +142,7 @@ pub trait FileLibrary { /// /// **Parameters** /// - `path` (`Option`): The directory path or glob pattern. Defaults to current working directory. + /// - `dir_self` (`Option`): Include self if file list target is a directory. Defaults to false. /// /// **Returns** /// - `List`: A list of dictionaries containing file details: @@ -152,11 +153,16 @@ pub trait FileLibrary { /// - `group` (`str`) /// - `permissions` (`str`) /// - `modified` (`str`) + /// - `times` (`dict`) /// - `type` (`str`: "File" or "Directory") /// /// **Errors** /// - Returns an error string if listing fails. - fn list(&self, path: Option) -> Result>, String>; + fn list( + &self, + path: Option, + dir_self: Option, + ) -> Result>, String>; #[eldritch_method] /// Lists all named pipes on the system. diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs index 5dc38158a..844d54e51 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs @@ -13,12 +13,21 @@ use eldritch_core::Value; #[cfg(unix)] use nix::unistd::{Gid, Group, Uid, User}; #[cfg(feature = "stdlib")] +use spin::RwLock; +#[cfg(feature = "stdlib")] use std::fs; #[cfg(feature = "stdlib")] use std::path::Path; +#[cfg(feature = "stdlib")] +use std::sync::Arc; +#[cfg(feature = "stdlib")] +use std::time::UNIX_EPOCH; #[cfg(feature = "stdlib")] -pub fn list(path: Option) -> Result>, String> { +pub fn list( + path: Option, + dir_self: Option, +) -> Result>, String> { let path = path.unwrap_or_else(|| { ::std::env::current_dir() .map(|p| p.to_string_lossy().to_string()) @@ -30,12 +39,13 @@ pub fn list(path: Option) -> Result>, String } }) }); - list_impl(path).map_err(|e| e.to_string()) + list_impl(path, dir_self.unwrap_or(false)).map_err(|e| e.to_string()) } #[cfg(not(feature = "stdlib"))] pub fn list( _path: Option, + dir_self: Option, ) -> Result< alloc::vec::Vec>, alloc::string::String, @@ -44,7 +54,7 @@ pub fn list( } #[cfg(feature = "stdlib")] -fn list_impl(path: String) -> AnyhowResult>> { +fn list_impl(path: String, dir_self: bool) -> AnyhowResult>> { use glob::glob; let mut final_res = Vec::new(); @@ -53,22 +63,73 @@ fn list_impl(path: String) -> AnyhowResult>> { for entry in glob(&path)? { match entry { Ok(path_buf) => { - // If I implement `handle_list` roughly: + // if we're not a directory (file), show self + // OR + // if we are a directory, only show self if the flag is set to true + if !path_buf.is_dir() || dir_self { + final_res.push(create_dict_from_file(&path_buf)?); + } + + // for dir, show subcontents if path_buf.is_dir() { for entry in fs::read_dir(&path_buf)? { let entry = entry?; final_res.push(create_dict_from_file(&entry.path())?); } - } else { - final_res.push(create_dict_from_file(&path_buf)?); } } Err(e) => eprintln!("Glob error: {e:?}"), } } + + // sort by absolute_path + final_res.sort_by_key(|k| k.get("absolute_path").cloned()); Ok(final_res) } +// get the timestamps of a metadata object and return it as a dictionary +#[cfg(feature = "stdlib")] +fn get_times_dict( + metadata: std::fs::Metadata, + mtime: std::io::Result, +) -> Value { + // create dictionary for times data + let mut times: BTreeMap = BTreeMap::new(); + + // add changed time (it's already in epoch format) if we're in unix + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + times.insert( + Value::String("changed".to_string()), + Value::Int(metadata.ctime()), + ); + } + + // add time information + let timestamps = [ + ("modified", mtime), + ("created", metadata.created()), + ("accessed", metadata.accessed()), + ]; + for timestamp_req in timestamps { + // if getting the timestamp was successful, add it + if let Ok(timestamp) = timestamp_req.1 { + // convert timestamp to epoch + let secs = match timestamp.duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_secs() as i64, + Err(err) => -(err.duration().as_secs() as i64), + }; + + // add timestamp to dict + times.insert(Value::String(timestamp_req.0.to_string()), Value::Int(secs)); + } + } + + // insert times section to the dictionary + return Value::Dictionary(Arc::new(RwLock::new(times))); +} + #[cfg(feature = "stdlib")] fn create_dict_from_file(path: &Path) -> AnyhowResult> { use alloc::format; @@ -137,13 +198,19 @@ fn create_dict_from_file(path: &Path) -> AnyhowResult> { Value::String(abs_path.to_string_lossy().to_string()), ); - // Times - if let Ok(modified) = metadata.modified() { + // cache modified time, then add it to both epoch and as a stringified version + let mtime = metadata.modified(); + + // turn modified time into a stringified version + if let Ok(modified) = mtime { let dt: chrono::DateTime = modified.into(); let formatted = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); dict.insert("modified".to_string(), Value::String(formatted)); } + // Add Time information + dict.insert("times".to_string(), get_times_dict(metadata, mtime)); + Ok(dict) } @@ -154,18 +221,38 @@ mod tests { use regex::bytes::Regex; use tempfile::NamedTempFile; + #[test] + fn test_list_includes_self_when_dir() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("a.txt"), b"hi").unwrap(); + let files = list(Some(dir.path().to_string_lossy().to_string()), Some(true)).unwrap(); + assert!(files.len() >= 2); // self + child + } + #[test] fn test_list_owner_group() { let tmp = NamedTempFile::new().unwrap(); let path = tmp.path().to_string_lossy().to_string(); - let files = list(Some(path)).unwrap(); + let files = list(Some(path), None).unwrap(); assert_eq!(files.len(), 1); let f = &files[0]; assert!(f.contains_key("owner")); assert!(f.contains_key("group")); assert!(f.contains_key("absolute_path")); + assert!(f.contains_key("times")); + // check times sub-dict + if let Value::Dictionary(d) = &f["times"] { + let inner = d.read(); + assert!(inner.contains_key(&Value::String("modified".into()))); + assert!(inner.contains_key(&Value::String("accessed".into()))); + assert!(inner.contains_key(&Value::String("created".into()))); + #[cfg(unix)] + assert!(inner.contains_key(&Value::String("changed".into()))); + } + + // check modified string assert!(f.contains_key("modified")); // Check absolute_path @@ -176,7 +263,7 @@ mod tests { panic!("absolute_path is not a string"); } - // Check modified time format + //Check modified time format if let Value::String(mod_time) = &f["modified"] { // Check format YYYY-MM-DD HH:MM:SS UTC let re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$").unwrap(); diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs index de3991ac3..a945adea7 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs @@ -78,8 +78,12 @@ impl FileLibrary for StdFileLibrary { is_file_impl::is_file(path) } - fn list(&self, path: Option) -> Result>, String> { - list_impl::list(path) + fn list( + &self, + path: Option, + dir_self: Option, + ) -> Result>, String> { + list_impl::list(path, dir_self) } fn list_named_pipes(&self, detailed: Option) -> Result {