Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 56 additions & 31 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dict>`
`file.list(path: str, dir_self: Optional<bool> = False) -> List<Dict>`

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<Dict>`. This field has no impact if `path` is not a directory.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does dir_self only show the local dir or optionally include it?
If only itself - can you name it only_dir_self to be more clear.
If optionally include the local dir - i think we should include that by default similar to ls

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay seems like it optionally includes the local dir. I think we should always include this.

@AmeliaYeah AmeliaYeah Jul 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i feel dir_self is good for backwards compatability, it's optional because in tomes prior to this change (where only path was the parameter), they obviously won't include the param and thus it'll function basically the same to how it did prior.

i wanted to include it by default but felt it might be better this way to prevent potential annoyances.

@AmeliaYeah AmeliaYeah Jul 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in hindsight, i'm not really sure what tomes are going to be hurt by it being included. i kind of originally made it so the dir was included by default but only added this after suggestions from @jabbate19 above.

🤷 idk, i suppose it doesn't hurt, i could include it by default/maybe remove the param for the sake of the function looking more polished tho

This function also supports globbing with `*` for example:

```python
Expand All @@ -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"
}
]
```

Expand Down
4 changes: 2 additions & 2 deletions implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ impl FileLibrary for FileLibraryFake {
}
}

fn list(&self, path: Option<String>) -> Result<Vec<BTreeMap<String, Value>>, String> {
fn list(&self, path: Option<String>, dir_self: Option<bool>) -> Result<Vec<BTreeMap<String, Value>>, String> {
let path = path.unwrap_or_else(|| "/".to_string());
let mut root = self.root.lock();
let parts = Self::normalize_path(&path);
Expand Down Expand Up @@ -486,7 +486,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()
Expand Down
4 changes: 3 additions & 1 deletion implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub trait FileLibrary {
///
/// **Parameters**
/// - `path` (`Option<str>`): The directory path or glob pattern. Defaults to current working directory.
/// - `dir_self` (`Option<bool>`): Include self if file list target is a directory. Defaults to false.
///
/// **Returns**
/// - `List<Dict>`: A list of dictionaries containing file details:
Expand All @@ -152,11 +153,12 @@ 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<String>) -> Result<Vec<BTreeMap<String, Value>>, String>;
fn list(&self, path: Option<String>, dir_self: Option<bool>) -> Result<Vec<BTreeMap<String, Value>>, String>;

#[eldritch_method]
/// Lists all named pipes on the system.
Expand Down
97 changes: 87 additions & 10 deletions implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ use nix::unistd::{Gid, Group, Uid, User};
use std::fs;
#[cfg(feature = "stdlib")]
use std::path::Path;
#[cfg(feature = "stdlib")]
use std::sync::Arc;
#[cfg(feature="stdlib")]
use spin::RwLock;
#[cfg(feature = "stdlib")]
use std::time::UNIX_EPOCH;

#[cfg(feature = "stdlib")]
pub fn list(path: Option<String>) -> Result<Vec<BTreeMap<String, Value>>, String> {
pub fn list(path: Option<String>, dir_self: Option<bool>) -> Result<Vec<BTreeMap<String, Value>>, String> {
let path = path.unwrap_or_else(|| {
::std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
Expand All @@ -30,12 +36,13 @@ pub fn list(path: Option<String>) -> Result<Vec<BTreeMap<String, Value>>, 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<alloc::string::String>,
dir_self: Option<bool>
) -> Result<
alloc::vec::Vec<alloc::collections::BTreeMap<alloc::string::String, eldritch_core::Value>>,
alloc::string::String,
Expand All @@ -44,7 +51,7 @@ pub fn list(
}

#[cfg(feature = "stdlib")]
fn list_impl(path: String) -> AnyhowResult<Vec<BTreeMap<String, Value>>> {
fn list_impl(path: String, dir_self: bool) -> AnyhowResult<Vec<BTreeMap<String, Value>>> {
use glob::glob;

let mut final_res = Vec::new();
Expand All @@ -53,22 +60,66 @@ fn list_impl(path: String) -> AnyhowResult<Vec<BTreeMap<String, Value>>> {
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<std::time::SystemTime>) -> Value {
// create dictionary for times data
let mut times: BTreeMap<Value, Value> = 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()));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_times_dict mixes epoch sources + silent omission (76-107) at list_impl.rs:77-106:

  • changed via MetadataExt::ctime() already epoch i64 (can be negative pre-1970). Others via SystemTime::duration_since(UNIX_EPOCH) errors pre-1970 and gets silently dropped. A pre-1970 file could expose only changed. Normalize or document omission.
  • cast_signed() on u64 truncates >i64::MAX; i64::try_from more explicit.
  • Style: return Value::Dictionary(...) + stray ; after } line 103 non-idiomatic – use tail expr Value::Dictionary(Arc::new(RwLock::new(times))).
  • Duplicate syscall: create_dict_from_file:177-182 calls modified() for legacy string then get_times_dict calls it again. Cache once.
  • Typo compatability line 177 → compatibility.

File: implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_impl.rs:77-106

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes applied, negative epochs supported for mtime/atime/crtime


// 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<BTreeMap<String, Value>> {
use alloc::format;
Expand Down Expand Up @@ -137,13 +188,19 @@ fn create_dict_from_file(path: &Path) -> AnyhowResult<BTreeMap<String, Value>> {
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<chrono::Utc> = 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)
}

Expand All @@ -154,18 +211,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"));
Comment thread
AmeliaYeah marked this conversation as resolved.
// 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
Expand All @@ -176,7 +253,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();
Expand Down
4 changes: 2 additions & 2 deletions implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ impl FileLibrary for StdFileLibrary {
is_file_impl::is_file(path)
}

fn list(&self, path: Option<String>) -> Result<Vec<BTreeMap<String, Value>>, String> {
list_impl::list(path)
fn list(&self, path: Option<String>, dir_self: Option<bool>) -> Result<Vec<BTreeMap<String, Value>>, String> {
list_impl::list(path, dir_self)
}

fn list_named_pipes(&self, detailed: Option<bool>) -> Result<Value, String> {
Expand Down
Loading