use anyhow::{Context, Result};
use libc::{SIGTERM, kill};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, warn};
use std::time::Instant;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
pub enabled: bool,
pub base_dir: PathBuf,
pub max_memory: u64,
pub max_cpu_time: u64,
pub max_processes: usize,
pub max_file_size: u64,
pub allowed_paths: Vec<PathBuf>,
pub blocked_paths: Vec<PathBuf>,
pub network_enabled: bool,
pub env_vars: HashMap<String, String>,
pub blocked_env_vars: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
pub memory: u64,
pub cpu_time: u64,
pub max_fds: usize,
pub max_processes: usize,
pub max_file_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxResult {
pub success: bool,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub execution_time_ms: u64,
pub memory_peak: u64,
pub cpu_time_used: u64,
pub files_created: Vec<PathBuf>,
pub network_connections: Vec<String>,
}
#[derive(Debug)]
pub struct SecuritySandbox {
config: SandboxConfig,
active_sandboxes: Arc<Mutex<HashMap<String, SandboxExecution>>>,
resource_usage: Arc<Mutex<ResourceTracker>>,
}
#[derive(Debug)]
struct SandboxExecution {
pid: u32,
work_dir: PathBuf,
start_time: std::time::Instant,
command: String,
}
#[derive(Debug, Default)]
struct ResourceTracker {
active_count: usize,
total_memory: u64,
total_cpu_time: u64,
peak_memory: u64,
}
impl SecuritySandbox {
pub fn new(config: SandboxConfig) -> Result<Self> {
if !config.base_dir.exists() {
std::fs::create_dir_all(&config.base_dir)
.context("Failed to create sandbox base directory")?;
}
info!("Security sandbox initialized with base dir: {:?}", config.base_dir);
Ok(Self {
config,
active_sandboxes: Arc::new(Mutex::new(HashMap::new()))
resource_usage: Arc::new(Mutex::new(ResourceTracker::default()))
})
}
pub async fn execute(&self, command: &str, args: &[&str]) -> Result<SandboxResult> {
let sandbox_id: _ = format!("sandbox_{}", std::process::id());
let work_dir: _ = self.create_sandbox_dir(&sandbox_id)?;
info!("Executing command in sandbox: {} {}", command, args.join(" "));
let limits: _ = self.create_resource_limits();
let start_time: _ = std::time::Instant::now();
let output: _ = Command::new(command)
.args(args)
.current_dir(&work_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.spawn()
.context("Failed to spawn sandboxed process")?;
let pid: _ = output.id();
{
let mut active = self.active_sandboxes.lock().unwrap();
active.insert(sandbox_id.clone(), SandboxExecution {
pid,
work_dir: work_dir.clone(),
start_time,
command: format!("{} {}", command, args.join(" ")),
});
}
let result: _ = output.wait_with_output()
.context("Failed to wait for sandboxed process")?;
let execution_time: _ = start_time.elapsed();
self.cleanup_sandbox_dir(&work_dir)?;
let sandbox_result: _ = SandboxResult {
success: result.status.success(),
exit_code: result.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&result.stdout).to_string(),
stderr: String::from_utf8_lossy(&result.stderr).to_string(),
execution_time_ms: execution_time.as_millis() as u64,
memory_peak: 0, cpu_time_used: 0, files_created: Vec::new(), network_connections: Vec::new(), };
info!("Sandbox execution completed: success={}, exit_code={}",
sandbox_result.success, sandbox_result.exit_code);
Ok(sandbox_result)
}
fn create_sandbox_dir(&self, sandbox_id: &str) -> Result<PathBuf> {
let work_dir: _ = self.config.base_dir.join(sandbox_id);
std::fs::create_dir_all(&work_dir)
.context("Failed to create sandbox directory")?;
#[cfg(unix)]
{
std::fs::set_permissions(&work_dir, std::fs::Permissions::from_mode(0o700))
.context("Failed to set sandbox directory permissions")?;
}
Ok(work_dir)
}
fn cleanup_sandbox_dir(&self, work_dir: &PathBuf) -> Result<()> {
if work_dir.exists() {
std::fs::remove_dir_all(work_dir)
.context("Failed to remove sandbox directory")?;
}
Ok(())
}
fn create_resource_limits(&self) -> ResourceLimits {
ResourceLimits {
memory: self.config.max_memory,
cpu_time: self.config.max_cpu_time,
max_fds: 1024,
max_processes: self.config.max_processes,
max_file_size: self.config.max_file_size,
}
}
pub fn is_path_allowed(&self, path: &PathBuf) -> bool {
for blocked in &self.config.blocked_paths {
if path.starts_with(blocked) {
return false;
}
}
for allowed in &self.config.allowed_paths {
if path.starts_with(allowed) {
return true;
}
}
if self.config.allowed_paths.is_empty() {
return path.starts_with(&self.config.base_dir);
}
false
}
pub fn get_stats(&self) -> HashMap<String, serde_json::Value> {
let mut stats = HashMap::new();
let active: _ = self.active_sandboxes.lock().unwrap();
let resources: _ = self.resource_usage.lock().unwrap();
stats.insert("active_sandboxes".to_string(), serde_json::Value::from(active.len());
stats.insert("total_memory_bytes".to_string(), serde_json::Value::from(resources.total_memory));
stats.insert("total_cpu_time_seconds".to_string(), serde_json::Value::from(resources.total_cpu_time));
stats.insert("peak_memory_bytes".to_string(), serde_json::Value::from(resources.peak_memory));
stats
}
pub fn terminate_sandbox(&self, sandbox_id: &str) -> Result<()> {
let mut active = self.active_sandboxes.lock().unwrap();
if let Some(execution) = active.remove(sandbox_id) {
#[cfg(unix)]
{
unsafe { kill(execution.pid as i32, SIGTERM) };
}
#[cfg(windows)]
{
Command::new("taskkill")
.args(&["/PID", &execution.pid.to_string(), "/F"])
.status()
.ok();
}
self.cleanup_sandbox_dir(&execution.work_dir)?;
info!("Terminated sandbox: {}", sandbox_id);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_sandbox_creation() {
let config: _ = SandboxConfig {
enabled: true,
base_dir: PathBuf::from("/tmp/amberjs-sandbox"),
max_memory: 1024 * 1024 * 1024, max_cpu_time: 60,
max_processes: 10,
max_file_size: 100 * 1024 * 1024, allowed_paths: Vec::new(),
blocked_paths: Vec::new(),
network_enabled: false,
env_vars: HashMap::new(),
blocked_env_vars: Vec::new(),
};
let sandbox: _ = SecuritySandbox::new(config);
assert!(sandbox.is_ok());
}
#[test]
fn test_path_allowed() {
let config: _ = SandboxConfig {
enabled: true,
base_dir: PathBuf::from("/tmp/amberjs-sandbox"),
max_memory: 1024 * 1024 * 1024,
max_cpu_time: 60,
max_processes: 10,
max_file_size: 100 * 1024 * 1024,
allowed_paths: vec![PathBuf::from("/tmp/amberjs-sandbox")],
blocked_paths: vec![PathBuf::from("/etc")],
network_enabled: false,
env_vars: HashMap::new(),
blocked_env_vars: Vec::new(),
};
let sandbox: _ = SecuritySandbox::new(config).unwrap();
assert!(sandbox.is_path_allowed(&PathBuf::from("/tmp/amberjs-sandbox/test"));
assert!(!sandbox.is_path_allowed(&PathBuf::from("/etc/passwd"));
}
#[test]
fn test_resource_limits() {
let config: _ = SandboxConfig {
enabled: true,
base_dir: PathBuf::from("/tmp/amberjs-sandbox"),
max_memory: 512 * 1024 * 1024, max_cpu_time: 30,
max_processes: 5,
max_file_size: 50 * 1024 * 1024, allowed_paths: Vec::new(),
blocked_paths: Vec::new(),
network_enabled: false,
env_vars: HashMap::new(),
blocked_env_vars: Vec::new(),
};
let sandbox: _ = SecuritySandbox::new(config).unwrap();
let limits: _ = sandbox.create_resource_limits();
assert_eq!(limits.memory, 512 * 1024 * 1024);
assert_eq!(limits.cpu_time, 30);
assert_eq!(limits.max_processes, 5);
assert_eq!(limits.max_file_size, 50 * 1024 * 1024);
}
}