use regex::Regex;
use std::collections::HashSet;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use wrkflw_logging;
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub max_execution_time: Duration,
pub max_memory_mb: u64,
pub max_cpu_percent: u64,
pub allowed_commands: HashSet<String>,
pub blocked_commands: HashSet<String>,
pub allow_network: bool,
pub max_processes: u32,
pub strict_mode: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
let mut allowed_commands = HashSet::new();
allowed_commands.insert("echo".to_string());
allowed_commands.insert("printf".to_string());
allowed_commands.insert("cat".to_string());
allowed_commands.insert("head".to_string());
allowed_commands.insert("tail".to_string());
allowed_commands.insert("grep".to_string());
allowed_commands.insert("sed".to_string());
allowed_commands.insert("awk".to_string());
allowed_commands.insert("sort".to_string());
allowed_commands.insert("uniq".to_string());
allowed_commands.insert("wc".to_string());
allowed_commands.insert("cut".to_string());
allowed_commands.insert("tr".to_string());
allowed_commands.insert("which".to_string());
allowed_commands.insert("pwd".to_string());
allowed_commands.insert("env".to_string());
allowed_commands.insert("date".to_string());
allowed_commands.insert("basename".to_string());
allowed_commands.insert("dirname".to_string());
allowed_commands.insert("ls".to_string());
allowed_commands.insert("find".to_string());
allowed_commands.insert("mkdir".to_string());
allowed_commands.insert("touch".to_string());
allowed_commands.insert("cp".to_string());
allowed_commands.insert("mv".to_string());
allowed_commands.insert("git".to_string());
allowed_commands.insert("cargo".to_string());
allowed_commands.insert("rustc".to_string());
allowed_commands.insert("rustfmt".to_string());
allowed_commands.insert("clippy".to_string());
allowed_commands.insert("npm".to_string());
allowed_commands.insert("yarn".to_string());
allowed_commands.insert("node".to_string());
allowed_commands.insert("python".to_string());
allowed_commands.insert("python3".to_string());
allowed_commands.insert("pip".to_string());
allowed_commands.insert("pip3".to_string());
allowed_commands.insert("java".to_string());
allowed_commands.insert("javac".to_string());
allowed_commands.insert("maven".to_string());
allowed_commands.insert("gradle".to_string());
allowed_commands.insert("go".to_string());
allowed_commands.insert("dotnet".to_string());
allowed_commands.insert("tar".to_string());
allowed_commands.insert("gzip".to_string());
allowed_commands.insert("gunzip".to_string());
allowed_commands.insert("zip".to_string());
allowed_commands.insert("unzip".to_string());
let mut blocked_commands = HashSet::new();
blocked_commands.insert("rm".to_string());
blocked_commands.insert("rmdir".to_string());
blocked_commands.insert("dd".to_string());
blocked_commands.insert("mkfs".to_string());
blocked_commands.insert("fdisk".to_string());
blocked_commands.insert("mount".to_string());
blocked_commands.insert("umount".to_string());
blocked_commands.insert("sudo".to_string());
blocked_commands.insert("su".to_string());
blocked_commands.insert("passwd".to_string());
blocked_commands.insert("chown".to_string());
blocked_commands.insert("chmod".to_string());
blocked_commands.insert("chgrp".to_string());
blocked_commands.insert("chroot".to_string());
blocked_commands.insert("nc".to_string());
blocked_commands.insert("netcat".to_string());
blocked_commands.insert("wget".to_string());
blocked_commands.insert("curl".to_string());
blocked_commands.insert("ssh".to_string());
blocked_commands.insert("scp".to_string());
blocked_commands.insert("rsync".to_string());
blocked_commands.insert("kill".to_string());
blocked_commands.insert("killall".to_string());
blocked_commands.insert("pkill".to_string());
blocked_commands.insert("nohup".to_string());
blocked_commands.insert("screen".to_string());
blocked_commands.insert("tmux".to_string());
blocked_commands.insert("systemctl".to_string());
blocked_commands.insert("service".to_string());
blocked_commands.insert("crontab".to_string());
blocked_commands.insert("at".to_string());
blocked_commands.insert("reboot".to_string());
blocked_commands.insert("shutdown".to_string());
blocked_commands.insert("halt".to_string());
blocked_commands.insert("poweroff".to_string());
Self {
max_execution_time: Duration::from_secs(300), max_memory_mb: 512,
max_cpu_percent: 80,
allowed_commands,
blocked_commands,
allow_network: false,
max_processes: 10,
strict_mode: true,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SandboxError {
#[error("Command blocked by security policy: {command}")]
BlockedCommand { command: String },
#[error("Dangerous command pattern detected: {pattern}")]
DangerousPattern { pattern: String },
#[error("Path access denied: {path}")]
PathAccessDenied { path: String },
#[error("Resource limit exceeded: {resource}")]
ResourceLimitExceeded { resource: String },
#[error("Execution timeout after {seconds} seconds")]
ExecutionTimeout { seconds: u64 },
#[error("Sandbox setup failed: {reason}")]
SandboxSetupError { reason: String },
#[error("Command execution failed: {reason}")]
ExecutionError { reason: String },
}
pub struct Sandbox {
config: SandboxConfig,
dangerous_patterns: Vec<Regex>,
}
impl Sandbox {
pub fn new(config: SandboxConfig) -> Result<Self, SandboxError> {
let dangerous_patterns = Self::compile_dangerous_patterns();
wrkflw_logging::info("Created new sandbox");
Ok(Self {
config,
dangerous_patterns,
})
}
pub async fn execute_command(
&self,
command: &[&str],
env_vars: &[(&str, &str)],
working_dir: &Path,
) -> Result<crate::container::ContainerOutput, SandboxError> {
if command.is_empty() {
return Err(SandboxError::ExecutionError {
reason: "Empty command".to_string(),
});
}
let command_str = command.join(" ");
self.validate_command(&command_str)?;
self.execute_with_limits(command, env_vars, working_dir)
.await
}
fn validate_command(&self, command_str: &str) -> Result<(), SandboxError> {
for pattern in &self.dangerous_patterns {
if pattern.is_match(command_str) {
wrkflw_logging::warning(&format!(
"{} Blocked dangerous command pattern: {}",
wrkflw_logging::symbols::BLOCKED,
command_str
));
return Err(SandboxError::DangerousPattern {
pattern: command_str.to_string(),
});
}
}
let command_parts = self.split_shell_command(command_str);
for part in command_parts {
let part = part.trim();
if part.is_empty() {
continue;
}
let base_command = part.split_whitespace().next().unwrap_or("");
let command_name = Path::new(base_command)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(base_command);
if self.is_shell_builtin(command_name) {
continue;
}
if self.config.blocked_commands.contains(command_name) {
wrkflw_logging::warning(&format!(
"{} Blocked command: {}",
wrkflw_logging::symbols::BLOCKED,
command_name
));
return Err(SandboxError::BlockedCommand {
command: command_name.to_string(),
});
}
if self.config.strict_mode && !self.config.allowed_commands.contains(command_name) {
wrkflw_logging::warning(&format!(
"{} Command not in whitelist (strict mode): {}",
wrkflw_logging::symbols::BLOCKED,
command_name
));
return Err(SandboxError::BlockedCommand {
command: command_name.to_string(),
});
}
}
wrkflw_logging::info(&format!(
"{} Command validation passed: {}",
wrkflw_logging::symbols::SUCCESS,
command_str
));
Ok(())
}
fn split_shell_command(&self, command_str: &str) -> Vec<String> {
let separators = ["&&", "||", ";", "|"];
let mut parts = vec![command_str.to_string()];
for separator in separators {
let mut new_parts = Vec::new();
for part in parts {
let split_parts: Vec<String> = part
.split(separator)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
new_parts.extend(split_parts);
}
parts = new_parts;
}
parts
}
fn is_shell_builtin(&self, command: &str) -> bool {
let builtins = [
"true", "false", "test", "[", "echo", "printf", "cd", "pwd", "export", "set", "unset",
"alias", "history", "jobs", "fg", "bg", "wait", "read",
];
builtins.contains(&command)
}
async fn execute_with_limits(
&self,
command: &[&str],
env_vars: &[(&str, &str)],
working_dir: &Path,
) -> Result<crate::container::ContainerOutput, SandboxError> {
let command_str = command.join(" ");
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd.arg(&command_str);
cmd.current_dir(working_dir);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
for (key, value) in env_vars {
if self.is_env_var_safe(key) {
cmd.env(key, value);
}
}
cmd.env("WRKFLW_SANDBOXED", "true");
cmd.env("WRKFLW_SANDBOX_MODE", "strict");
let timeout_duration = self.config.max_execution_time;
wrkflw_logging::info(&format!(
"🏃 Executing sandboxed command: {} (timeout: {}s)",
command.join(" "),
timeout_duration.as_secs()
));
let start_time = std::time::Instant::now();
let result = tokio::time::timeout(timeout_duration, async {
let output = cmd.output().map_err(|e| SandboxError::ExecutionError {
reason: format!("Command execution failed: {}", e),
})?;
Ok(crate::container::ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
})
})
.await;
let execution_time = start_time.elapsed();
match result {
Ok(output_result) => {
wrkflw_logging::info(&format!(
"{} Sandboxed command completed in {:.2}s",
wrkflw_logging::symbols::SUCCESS,
execution_time.as_secs_f64()
));
output_result
}
Err(_) => {
wrkflw_logging::warning(&format!(
"{} Sandboxed command timed out after {:.2}s",
wrkflw_logging::symbols::WARNING,
timeout_duration.as_secs_f64()
));
Err(SandboxError::ExecutionTimeout {
seconds: timeout_duration.as_secs(),
})
}
}
}
fn is_env_var_safe(&self, key: &str) -> bool {
let dangerous_env_vars = [
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"PATH",
"HOME",
"SHELL",
];
!dangerous_env_vars.contains(&key)
}
fn compile_dangerous_patterns() -> Vec<Regex> {
let patterns = [
r"rm\s+.*-rf?\s*/", r"dd\s+.*of=/dev/", r">\s*/dev/sd[a-z]", r"mkfs\.", r"fdisk\s+/dev/", r"mount\s+.*\s+/", r"chroot\s+/", r"sudo\s+", r"su\s+", r"bash\s+-c\s+.*rm.*-rf", r"sh\s+-c\s+.*rm.*-rf", r"eval\s+.*rm.*-rf", r":\(\)\{.*;\};:", r"/proc/sys/", r"/etc/passwd", r"/etc/shadow", r"nc\s+.*-e", r"wget\s+.*\|\s*sh", r"curl\s+.*\|\s*sh", ];
patterns
.iter()
.filter_map(|pattern| {
Regex::new(pattern)
.map_err(|e| {
wrkflw_logging::warning(&format!(
"Invalid regex pattern {}: {}",
pattern, e
));
e
})
.ok()
})
.collect()
}
}
pub fn create_workflow_sandbox_config() -> SandboxConfig {
SandboxConfig {
max_execution_time: Duration::from_secs(1800), max_memory_mb: 2048, max_processes: 50,
allow_network: true,
strict_mode: false,
..Default::default()
}
}
pub fn create_strict_sandbox_config() -> SandboxConfig {
let allowed_commands = ["echo", "cat", "ls", "pwd", "date"]
.iter()
.map(|s| s.to_string())
.collect();
SandboxConfig {
max_execution_time: Duration::from_secs(60), max_memory_mb: 128, max_processes: 5,
allow_network: false,
strict_mode: true,
allowed_commands,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dangerous_pattern_detection() {
let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
assert!(sandbox.validate_command("rm -rf /").is_err());
assert!(sandbox
.validate_command("dd if=/dev/zero of=/dev/sda")
.is_err());
assert!(sandbox.validate_command("sudo rm -rf /home").is_err());
assert!(sandbox.validate_command("bash -c 'rm -rf /'").is_err());
assert!(sandbox.validate_command("echo hello").is_ok());
assert!(sandbox.validate_command("ls -la").is_ok());
assert!(sandbox.validate_command("cargo build").is_ok());
}
#[test]
fn test_command_whitelist() {
let config = create_strict_sandbox_config();
let sandbox = Sandbox::new(config).unwrap();
assert!(sandbox.validate_command("echo hello").is_ok());
assert!(sandbox.validate_command("ls").is_ok());
assert!(sandbox.validate_command("git clone").is_err());
assert!(sandbox.validate_command("cargo build").is_err());
}
}