From 873033754809045ee2a0d124fa0920bec6d036dc Mon Sep 17 00:00:00 2001
From: Joe Abbate <40615740+jabbate19@users.noreply.github.com>
Date: Tue, 6 Feb 2024 17:48:00 -0500
Subject: [PATCH] [feature] Eldritch file.follow (#546)
* Implement file.follow
* Fix follow to only do one file
* Clean up follow
* Update contribution docs for current practice
---
docs/_docs/dev-guide/eldritch.md | 27 ++++++-------
docs/_docs/user-guide/eldritch.md | 11 ++++++
implants/Cargo.toml | 1 +
implants/lib/eldritch/Cargo.toml | 1 +
implants/lib/eldritch/src/file.rs | 7 ++++
implants/lib/eldritch/src/file/follow_impl.rs | 38 +++++++++++++++++++
implants/lib/eldritch/src/runtime.rs | 2 +-
7 files changed, 70 insertions(+), 17 deletions(-)
create mode 100644 implants/lib/eldritch/src/file/follow_impl.rs
diff --git a/docs/_docs/dev-guide/eldritch.md b/docs/_docs/dev-guide/eldritch.md
index 1cf4d8f2..c93bde8a 100644
--- a/docs/_docs/dev-guide/eldritch.md
+++ b/docs/_docs/dev-guide/eldritch.md
@@ -131,25 +131,20 @@ mod tests {
}
```
-### `eldritch/lib.rs` tests
+### `eldritch/runtime.rs` tests
-Lastly you'll need to add your function to the `eldritch/lib.rs` integration test.
+Lastly you'll need to add your function to the `eldritch/runtime.rs` integration test. Add your function to it's respective parent binding in alphabetical order.
```rust
- #[test]
- fn test_library_bindings() {
- let globals = get_eldritch().unwrap();
- let mut a = Assert::new();
- a.globals(globals);
- a.all_true(
- r#"
-dir(file) == ["append", "compress", "copy", "download", "exists", "hash", "is_dir", "is_file", "mkdir", "read", "remove", "rename", "replace", "replace_all", "template", "timestomp", "write"]
-dir(process) == ["kill", "list", "name"]
-dir(sys) == ["dll_inject", "exec", "is_linux", "is_macos", "is_windows", "shell"]
-dir(pivot) == ["arp_scan", "bind_proxy", "ncat", "port_forward", "port_scan", "smb_exec", "ssh_exec", "ssh_password_spray"]
-dir(assets) == ["copy","list"]
-"#,
- );
+ // Binding for the "file" functions
+ file_bindings: TestCase {
+ tome: Tome {
+ eldritch: String::from("print(dir(file))"),
+ parameters: HashMap::new(),
+ file_names: Vec::new(),
+ },
+ want_output: String::from(r#"["append", "compress", "copy", "download", "exists", "find", "follow", "is_dir", "is_file", "list", "mkdir", "moveto", "read", "remove", "replace", "replace_all", "template", "timestomp", "write"]"#),
+ want_error: None,
}
```
diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md
index e489f809..457b981b 100644
--- a/docs/_docs/user-guide/eldritch.md
+++ b/docs/_docs/user-guide/eldritch.md
@@ -266,6 +266,17 @@ The file.download method downloads a file at the URI specified in `uri` t
The file.exists method checks if a file or directory exists at the path specified.
+### file.follow
+
+`file.follow(path: str, fn: function(str)) -> None`
+
+The file.follow method will call `fn(line)` for any new `line` that is added to the file (such as from `bash_history` and other logs).
+
+```python
+# Print every line added to bob's bash history
+file.follow('/home/bob/.bash_history', print)
+```
+
### file.is_dir
`file.is_dir(path: str) -> bool`
diff --git a/implants/Cargo.toml b/implants/Cargo.toml
index e8d293ed..c00df43b 100644
--- a/implants/Cargo.toml
+++ b/implants/Cargo.toml
@@ -32,6 +32,7 @@ md5 = "0.7.0"
netstat2 = "0.9.1"
network-interface = "1.0.1"
nix = "0.26.1"
+notify = "6.1.1"
object = "0.31.1"
openssl = "0.10.55"
pnet = "0.34.0"
diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml
index c6a6845f..3ee30108 100644
--- a/implants/lib/eldritch/Cargo.toml
+++ b/implants/lib/eldritch/Cargo.toml
@@ -27,6 +27,7 @@ log = { workspace = true }
md5 = { workspace = true }
netstat2 = { workspace = true }
nix = { workspace = true }
+notify = { workspace = true }
object = { workspace = true }
openssl = { workspace = true, features = ["vendored"] }
prost = { workspace = true}
diff --git a/implants/lib/eldritch/src/file.rs b/implants/lib/eldritch/src/file.rs
index 718a18ed..7a2a9ce6 100644
--- a/implants/lib/eldritch/src/file.rs
+++ b/implants/lib/eldritch/src/file.rs
@@ -4,6 +4,7 @@ mod copy_impl;
mod download_impl;
mod exists_impl;
mod find_impl;
+mod follow_impl;
mod is_dir_impl;
mod is_file_impl;
mod list_impl;
@@ -23,6 +24,7 @@ use derive_more::Display;
use serde::{Serialize, Serializer};
use starlark::collections::SmallMap;
use starlark::environment::{Methods, MethodsBuilder, MethodsStatic};
+use starlark::eval::Evaluator;
use starlark::values::dict::Dict;
use starlark::values::none::NoneType;
use starlark::values::{
@@ -181,4 +183,9 @@ fn methods(builder: &mut MethodsBuilder) {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
find_impl::find(path, name, file_type, permissions, modified_time, create_time)
}
+ fn follow<'v>(this: FileLibrary, path: String, f: Value<'v>, eval: &mut Evaluator<'v, '_>) -> anyhow::Result {
+ if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
+ follow_impl::follow(path, f, eval)?;
+ Ok(NoneType{})
+ }
}
diff --git a/implants/lib/eldritch/src/file/follow_impl.rs b/implants/lib/eldritch/src/file/follow_impl.rs
new file mode 100644
index 00000000..1326af17
--- /dev/null
+++ b/implants/lib/eldritch/src/file/follow_impl.rs
@@ -0,0 +1,38 @@
+use std::io::{Seek, BufReader, BufRead};
+use notify::{Watcher, RecursiveMode, RecommendedWatcher, Config};
+
+use anyhow::{anyhow, Result};
+use starlark::{eval::Evaluator, values::Value};
+
+pub fn follow<'v>(path: String, f: Value<'v>, eval: &mut Evaluator<'v, '_>) -> Result<()> {
+ let starlark_heap = eval.heap();
+ // get pos to end of file
+ let mut file = std::fs::File::open(&path)?;
+ let mut pos = std::fs::metadata(&path)?.len();
+
+ // set up watcher
+ let (tx, rx) = std::sync::mpsc::channel();
+ let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
+ watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?;
+
+ // watch
+ for _event in rx.into_iter().flatten() {
+ // ignore any event that didn't change the pos
+ if file.metadata()?.len() == pos {
+ continue;
+ }
+
+ // read from pos to end of file
+ file.seek(std::io::SeekFrom::Start(pos))?;
+
+ // update post to end of file
+ pos = file.metadata()?.len();
+
+ let reader = BufReader::new(&file);
+ for line in reader.lines() {
+ let val = starlark_heap.alloc(line?.to_string());
+ eval.eval_function(f, &[val], &[])?;
+ }
+ }
+ Ok(())
+}
\ No newline at end of file
diff --git a/implants/lib/eldritch/src/runtime.rs b/implants/lib/eldritch/src/runtime.rs
index 92a2cd72..e7d3affe 100644
--- a/implants/lib/eldritch/src/runtime.rs
+++ b/implants/lib/eldritch/src/runtime.rs
@@ -456,7 +456,7 @@ mod tests {
parameters: HashMap::new(),
file_names: Vec::new(),
},
- want_output: String::from(r#"["append", "compress", "copy", "download", "exists", "find", "is_dir", "is_file", "list", "mkdir", "moveto", "read", "remove", "replace", "replace_all", "template", "timestomp", "write"]"#),
+ want_output: String::from(r#"["append", "compress", "copy", "download", "exists", "find", "follow", "is_dir", "is_file", "list", "mkdir", "moveto", "read", "remove", "replace", "replace_all", "template", "timestomp", "write"]"#),
want_error: None,
},
process_bindings: TestCase {