Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/e2e-repl-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: ⚡ Setup Golang
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.91.1'
toolchain: 'stable'
default: true
profile: minimal
components: rustfmt, clippy
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/e2e-tome-timeout.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: E2E Tome Timeout Test 🧪

on:
workflow_dispatch: ~
push:
branches: [ main ]
pull_request:
branches: [ main ]
merge_group:

jobs:
e2e_tome_timeout_test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: ⚡ Setup Golang
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 'stable'
default: true
profile: minimal
components: rustfmt, clippy

- name: ⚡ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tavern/internal/www/package-lock.json

- name: 📦 Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgbm-dev libx11-dev libssl-dev protobuf-compiler pkg-config libclang-dev libxcb1-dev libxrandr-dev libdbus-1-dev libpipewire-0.3-dev libwayland-dev libegl-dev

- name: 🔨 Build Tavern
run: |
go mod download
go build -v -o tavern_bin ./tavern
- name: 🚀 Run Tavern
env:
HTTP_LISTEN_ADDR: ":8000"
run: |
./tavern_bin &
echo "Waiting for Tavern to start..."
# Wait for port 8000
timeout 30 sh -c 'until nc -z $0 $1; do sleep 1; done' localhost 8000
- name: 🤖 Run Agent
working-directory: implants/imix
env:
IMIX_CALLBACK_URI: "http://localhost:8000"
IMIX_CALLBACK_INTERVAL: 1
run: |
# Fetch the pubkey and verify it's not empty
PUBKEY=$(curl -s http://localhost:8000/status | jq -r .Pubkey)
if [ -z "$PUBKEY" ] || [ "$PUBKEY" == "null" ]; then
echo "Error: Could not fetch Pubkey from Tavern"
exit 1
fi
export IMIX_SERVER_PUBKEY=$PUBKEY
echo "Got pubkey: $IMIX_SERVER_PUBKEY"

echo "Building imix..."
cargo build --bin imix --target-dir ./build
# Run agent and pipe logs to a file
./build/debug/imix > agent.log 2>&1 &

# Give the agent a moment to perform the initial handshake
echo "Agent started. Waiting for initial callback..."
sleep 5
- name: 🎭 Install Playwright
working-directory: tests/e2e
run: |
npm ci
npx playwright install --with-deps chromium
- name: 🧪 Run E2E Tests
working-directory: tests/e2e
run: |
npx playwright test tests/tome_timeout.spec.ts
- name: 📂 Upload Service Logs
if: always() # Runs even if tests fail
uses: actions/upload-artifact@v4
with:
name: e2e-logs
path: |
tavern.log
implants/imix/agent.log
2 changes: 2 additions & 0 deletions implants/lib/eldritch/eldritch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,5 @@ mod input_params_test;

#[cfg(test)]
mod process_report_test;
#[cfg(test)]
mod tome_timeout_test;
164 changes: 164 additions & 0 deletions implants/lib/eldritch/eldritch/src/tome_timeout_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#[cfg(test)]
mod tests {
use crate::Interpreter;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use eldritch_agent::Context;
use eldritch_libassets::std::EmptyAssets;
use pb::c2;
use pb::c2::TaskContext;
use std::sync::Arc;
use std::time::Duration;

struct MockAgent;

impl crate::Agent for MockAgent {
fn report_process_list(
&self,
_req: c2::ReportProcessListRequest,
) -> Result<c2::ReportProcessListResponse, String> {
Ok(c2::ReportProcessListResponse::default())
}
fn fetch_asset(&self, _req: c2::FetchAssetRequest) -> Result<Vec<u8>, String> {
Ok(Vec::new())
}
fn report_credential(
&self,
_req: c2::ReportCredentialRequest,
) -> Result<c2::ReportCredentialResponse, String> {
Ok(c2::ReportCredentialResponse::default())
}
fn report_file(
&self,
_req: std::sync::mpsc::Receiver<c2::ReportFileRequest>,
) -> Result<c2::ReportFileResponse, String> {
Ok(c2::ReportFileResponse::default())
}
fn report_output(
&self,
_req: c2::ReportOutputRequest,
) -> Result<c2::ReportOutputResponse, String> {
Ok(c2::ReportOutputResponse::default())
}
fn start_reverse_shell(
&self,
_context: Context,
_cmd: Option<String>,
) -> Result<(), String> {
Ok(())
}
fn create_portal(&self, _context: Context) -> Result<(), String> {
Ok(())
}
fn start_repl_reverse_shell(&self, _context: Context) -> Result<(), String> {
Ok(())
}
fn claim_tasks(
&self,
_req: c2::ClaimTasksRequest,
) -> Result<c2::ClaimTasksResponse, String> {
Ok(c2::ClaimTasksResponse::default())
}
fn get_config(&self) -> Result<BTreeMap<String, String>, String> {
Ok(BTreeMap::new())
}
fn get_transport(&self) -> Result<String, String> {
Ok("http".to_string())
}
fn set_transport(&self, _transport: String) -> Result<(), String> {
Ok(())
}
fn list_transports(&self) -> Result<Vec<String>, String> {
Ok(Vec::new())
}
fn get_callback_interval(&self) -> Result<u64, String> {
Ok(10)
}
fn set_callback_interval(&self, _interval: u64) -> Result<(), String> {
Ok(())
}
fn set_callback_uri(&self, _uri: String) -> Result<(), String> {
Ok(())
}
fn list_callback_uris(&self) -> Result<BTreeSet<String>, String> {
Ok(BTreeSet::new())
}
fn get_active_callback_uri(&self) -> Result<String, String> {
Ok(String::new())
}
fn get_next_callback_uri(&self) -> Result<String, String> {
Ok(String::new())
}
fn add_callback_uri(&self, _uri: String) -> Result<(), String> {
Ok(())
}
fn remove_callback_uri(&self, _uri: String) -> Result<(), String> {
Ok(())
}
fn list_tasks(&self) -> Result<Vec<c2::Task>, String> {
Ok(Vec::new())
}
fn stop_task(&self, _task_id: i64) -> Result<(), String> {
Ok(())
}
}

fn run_script_with_timeout(script: &str) {
let (tx, rx) = std::sync::mpsc::channel();
let script = script.to_string();

std::thread::spawn(move || {
let agent = Arc::new(MockAgent);
let task_context = TaskContext {
task_id: 123,
jwt: "test_jwt".to_string(),
};
let context = Context::Task(task_context);
let backend = Arc::new(EmptyAssets {});

let mut interp = Interpreter::new().with_default_libs().with_context(
agent,
context,
Vec::new(),
backend,
);

let res = interp.interpret(&script);
let _ = tx.send(res);
});

match rx.recv_timeout(Duration::from_secs(10)) {
Ok(Ok(_)) => {} // Script finished successfully
Ok(Err(e)) => panic!("Script failed: {:?}", e),
Err(_) => panic!("Script timed out after 10 seconds!"),
}
}

#[test]
fn test_process_list_tome_timeout() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let path = std::path::PathBuf::from(manifest_dir)
.join("../../../../tavern/tomes/process_list/main.eldritch");
let script = std::fs::read_to_string(path).unwrap();
run_script_with_timeout(&script);
}

#[test]
fn test_netstat_tome_timeout() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let path = std::path::PathBuf::from(manifest_dir)
.join("../../../../tavern/tomes/netstat/main.eldritch");
let script = std::fs::read_to_string(path).unwrap();
run_script_with_timeout(&script);
}

#[test]
fn test_get_net_info_tome_timeout() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let path = std::path::PathBuf::from(manifest_dir)
.join("../../../../tavern/tomes/get_net_info/main.eldritch");
let script = std::fs::read_to_string(path).unwrap();
run_script_with_timeout(&script);
}
}
60 changes: 60 additions & 0 deletions tests/e2e/tests/tome_timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { test, expect } from '@playwright/test';

test.describe('Tome Execution Timeout Tests', () => {
const tomesToTest = ['Process list', 'Netstat', 'Get network info'];

for (const tome of tomesToTest) {
test(`End-to-end execution of ${tome} tome`, async ({ page }) => {
// 1. Navigate to /createQuest
console.log(`Navigating to /createQuest for ${tome}`);
await page.goto('/createQuest');

// 2. Wait for beacons to load
console.log('Waiting for beacons to load');
await expect(page.getByText('Loading beacons...')).toBeHidden({ timeout: 15000 });

// Select the first beacon
const beacons = page.locator('.chakra-card input[type="checkbox"]');
await expect(beacons.first()).toBeVisible({ timeout: 10000 });
console.log('Selecting beacon');
await beacons.first().check({ force: true });

// Click Continue (Beacon Step)
console.log('Clicking Continue (Beacon)');
await page.getByRole('button', { name: 'Continue' }).click();

// 3. Select the target tome
console.log(`Selecting Tome: ${tome}`);
await expect(page.getByText('Loading tomes...')).toBeHidden();
await expect(page.getByText(tome)).toBeVisible({ timeout: 10000 });
await page.getByText(tome).click();

// Click Continue (Tome Step)
console.log('Clicking Continue (Tome)');
await page.getByRole('button', { name: 'Continue' }).click();

// 4. Submit Quest
console.log('Submitting Quest');
await page.getByRole('button', { name: 'Submit' }).click();

// 5. Wait for execution
// The tomes hang due to a bug, so we want to wait the full timeout (15s) and check they have NOT finished.
console.log('Waiting for execution (up to 15s)');
await page.waitForTimeout(15000);

// Reload to refresh the task status
await page.reload();

// 6. Check that the task has NOT finished
console.log('Checking that task did NOT finish');

// The TaskTimeStamp component renders "Finished at <time> on <date>" when execFinishedAt is set.
// We expect it NOT to be visible because the tome execution should have timed out/hung.
// This assertion validates the bug is correctly caught by the test.
const finishedAtText = page.getByText(/Finished at/);
await expect(finishedAtText).not.toBeVisible();

console.log(`Test for ${tome} Complete`);
});
}
});
Loading