Files
agent-sandbox/tests/integration.rs

304 lines
8.3 KiB
Rust

use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn sandbox(extra_args: &[&str]) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox"));
cmd.args(extra_args);
cmd
}
struct CleanupFile(&'static str);
impl Drop for CleanupFile {
fn drop(&mut self) {
let _ = fs::remove_file(self.0);
}
}
#[test]
fn cwd_is_writable() {
let _cleanup = CleanupFile("./sandbox_canary");
let output = sandbox(&[])
.args(["--", "bash", "-c", "touch ./sandbox_canary && echo ok"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("ok"),
"expected 'ok' in stdout, got: {stdout}"
);
}
#[test]
fn host_fs_is_readonly() {
let output = sandbox(&[])
.args(["--", "bash", "-c", "touch /etc/pwned 2>&1 || echo readonly"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("readonly"),
"expected 'readonly' in stdout, got: {stdout}"
);
assert!(!std::path::Path::new("/etc/pwned").exists());
}
#[test]
fn ssh_dir_is_hidden() {
let output = sandbox(&[])
.args(["--", "bash", "-c", "ls ~/.ssh 2>/dev/null | wc -l"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(stdout, "0", "expected empty ~/.ssh, got {stdout} entries");
}
#[test]
fn no_net_blocks_network() {
let output = sandbox(&["--no-net"])
.args([
"--",
"bash",
"-c",
"curl -s --max-time 2 http://1.1.1.1 2>&1; echo $?",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.trim().ends_with("0"),
"expected curl to fail, got: {stdout}"
);
}
#[test]
fn hardened_pid_namespace() {
let output = sandbox(&["--hardened"])
.args(["--", "bash", "-c", "ls /proc | grep -cE '^[0-9]+$'"])
.output()
.expect("agent-sandbox binary failed to execute");
let count: u32 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.unwrap_or(999);
assert!(
count < 10,
"expected isolated PID namespace with few PIDs, got {count}"
);
}
#[test]
fn whitelist_hides_home_contents() {
let output = sandbox(&["--whitelist"])
.args(["--", "bash", "-c", "ls ~/Documents 2>&1 || echo hidden"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hidden"),
"expected ~/Documents to be hidden, got: {stdout}"
);
}
#[test]
fn extra_ro_mount() {
let dir = TempDir::new().expect("failed to create temp dir");
fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file");
let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--ro", dir_str])
.args([
"--",
"bash",
"-c",
&format!("cat {dir_str}/hello.txt && touch {dir_str}/new 2>&1 || echo readonly"),
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hi"),
"expected file content 'hi', got: {stdout}"
);
assert!(
stdout.contains("readonly"),
"expected ro mount to block writes, got: {stdout}"
);
}
#[test]
fn extra_rw_mount() {
let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--rw", dir_str])
.args([
"--",
"bash",
"-c",
&format!("touch {dir_str}/canary && echo ok"),
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("ok"), "expected 'ok', got: {stdout}");
assert!(
dir.path().join("canary").exists(),
"canary file should exist on host after rw mount"
);
}
#[test]
fn chdir_override() {
let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--chdir", dir_str])
.args(["--", "bash", "-c", "pwd"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, dir_str,
"expected cwd to be {dir_str}, got: {stdout}"
);
}
#[test]
fn chdir_under_hardened_tmp() {
let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--hardened", "--chdir", dir_str])
.args(["--", "bash", "-c", "pwd && touch ./ok && echo done"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("done"),
"expected chdir under /tmp to work with --hardened, got: {stdout}"
);
}
#[test]
fn dry_run_prints_and_exits() {
let output = sandbox(&["--dry-run"])
.args(["--", "bash", "-c", "exit 42"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("bwrap"),
"expected bwrap command in dry-run output, got: {stdout}"
);
assert!(
output.status.success(),
"dry-run should exit 0, not 42 from the inner command"
);
}
#[test]
fn blacklist_overlays_survive_tmp_bind() {
fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel");
let _cleanup = CleanupFile("/tmp/ssh-sandbox-test");
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"cat /tmp/ssh-sandbox-test 2>/dev/null && echo LEAKED || echo HIDDEN",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HIDDEN"),
"expected /tmp/ssh-* to be hidden in blacklist mode, got: {stdout}"
);
assert!(
!stdout.contains("LEAKED"),
"/tmp/ssh-sandbox-test was readable inside the sandbox"
);
}
#[test]
fn relative_chdir_works() {
let output = sandbox(&["--chdir", "src"])
.args(["--", "bash", "-c", "pwd"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --chdir should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
stdout.ends_with("/src"),
"expected cwd ending in /src, got: {stdout}"
);
}
#[test]
fn relative_rw_path_works() {
let output = sandbox(&["--rw", "src"])
.args(["--", "bash", "-c", "echo ok"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --rw should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn relative_ro_path_works() {
let output = sandbox(&["--ro", "src"])
.args(["--", "bash", "-c", "echo ok"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --ro should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn rw_missing_path_errors() {
let output = sandbox(&["--rw", "/nonexistent/xyz"])
.args(["--", "true"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
!output.status.success(),
"expected non-zero exit for missing --rw path"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("/nonexistent/xyz"),
"expected path in error message, got: {stderr}"
);
}