diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 914225d9a..403def73c 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -731,41 +731,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. Note that the directory itself (in this example, `/tmp/some_dir`) will also be listed with the respective information. + +```python +print(file.list("/tmp/some_dir")) +``` + +**NOTE:** On systems without a specific time field being tracked, the field is omitted. 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", + "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/lib.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs index e5c69525c..eacaf6c60 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs @@ -152,6 +152,7 @@ pub trait FileLibrary { /// - `group` (`str`) /// - `permissions` (`str`) /// - `modified` (`str`) + /// - `times` (`dict`) /// - `type` (`str`: "File" or "Directory") /// /// **Errors** 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 15f4e2b18..6040f56f9 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,9 +13,15 @@ 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> { @@ -53,22 +59,64 @@ fn list_impl(path: String) -> AnyhowResult>> { for entry in glob(&path)? { match entry { Ok(path_buf) => { - // If I implement `handle_list` roughly: + // show information about the directory/file + // if it is a directory, show subcontents + final_res.push(create_dict_from_file(&path_buf)?); 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) -> 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", metadata.modified()), + ("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 + if let Ok(ts) = jiff::Timestamp::try_from(timestamp) { + times.insert( + Value::String(timestamp_req.0.to_string()), + Value::Int(ts.as_second()), + ); + } + } + } + + // 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; @@ -145,6 +193,9 @@ fn create_dict_from_file(path: &Path) -> AnyhowResult> { } } + // Add Time information + dict.insert("times".to_string(), get_times_dict(metadata)); + Ok(dict) } @@ -153,20 +204,40 @@ fn create_dict_from_file(path: &Path) -> AnyhowResult> { mod tests { use super::*; use regex::bytes::Regex; - use tempfile::NamedTempFile; + use tempfile::{NamedTempFile, TempDir}; #[test] fn test_list_owner_group() { + // check if listing a directory shows itself + let tmp_dir = TempDir::new().unwrap(); + let tmp_dir_path = tmp_dir.path().to_string_lossy().to_string(); + let tmp_dir_files = list(Some(tmp_dir_path)).unwrap(); + assert_eq!(tmp_dir_files.len(), 1); + + // create regular file let tmp = NamedTempFile::new().unwrap(); let path = tmp.path().to_string_lossy().to_string(); - let files = list(Some(path)).unwrap(); assert_eq!(files.len(), 1); + + // perform remaining tests 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 @@ -177,7 +248,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();