//! Bubblewrap sandbox (user namespaces for Linux/macOS) use crate::security::traits::Sandbox; use std::process::Command; /// Bubblewrap sandbox backend #[derive(Debug, Clone, Default)] pub struct BubblewrapSandbox; impl BubblewrapSandbox { pub fn new() -> std::io::Result { if Self::is_installed() { Ok(Self) } else { Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Bubblewrap not found", )) } } pub fn probe() -> std::io::Result { Self::new() } fn is_installed() -> bool { Command::new("bwrap") .arg("--version") .output() .map(|o| o.status.success()) .unwrap_or(false) } } impl Sandbox for BubblewrapSandbox { fn wrap_command(&self, cmd: &mut Command) -> std::io::Result<()> { let program = cmd.get_program().to_string_lossy().to_string(); let args: Vec = cmd .get_args() .map(|s| s.to_string_lossy().to_string()) .collect(); let mut bwrap_cmd = Command::new("bwrap"); bwrap_cmd.args([ "--ro-bind", "/usr", "/usr", "--dev", "/dev", "--proc", "/proc", "--bind", "/tmp", "/tmp", "--unshare-all", "--die-with-parent", ]); bwrap_cmd.arg(&program); bwrap_cmd.args(&args); *cmd = bwrap_cmd; Ok(()) } fn is_available(&self) -> bool { Self::is_installed() } fn name(&self) -> &str { "bubblewrap" } fn description(&self) -> &str { "User namespace sandbox (requires bwrap)" } } #[cfg(test)] mod tests { use super::*; #[test] fn bubblewrap_sandbox_name() { let sandbox = BubblewrapSandbox; assert_eq!(sandbox.name(), "bubblewrap"); } #[test] fn bubblewrap_is_available_only_if_installed() { // Result depends on whether bwrap is installed let sandbox = BubblewrapSandbox; let _available = sandbox.is_available(); // Either way, the name should still work assert_eq!(sandbox.name(), "bubblewrap"); } // ── §1.1 Sandbox isolation flag tests ────────────────────── #[test] fn bubblewrap_wrap_command_includes_isolation_flags() { let sandbox = BubblewrapSandbox; let mut cmd = Command::new("echo"); cmd.arg("hello"); sandbox.wrap_command(&mut cmd).unwrap(); assert_eq!( cmd.get_program().to_string_lossy(), "bwrap", "wrapped command should use bwrap as program" ); let args: Vec = cmd .get_args() .map(|s| s.to_string_lossy().to_string()) .collect(); assert!( args.contains(&"--unshare-all".to_string()), "must include --unshare-all for namespace isolation" ); assert!( args.contains(&"--die-with-parent".to_string()), "must include --die-with-parent to prevent orphan processes" ); assert!( !args.contains(&"--share-net".to_string()), "must NOT include --share-net (network should be blocked)" ); } #[test] fn bubblewrap_wrap_command_preserves_original_command() { let sandbox = BubblewrapSandbox; let mut cmd = Command::new("ls"); cmd.arg("-la"); cmd.arg("/tmp"); sandbox.wrap_command(&mut cmd).unwrap(); let args: Vec = cmd .get_args() .map(|s| s.to_string_lossy().to_string()) .collect(); assert!( args.contains(&"ls".to_string()), "original program must be passed as argument" ); assert!( args.contains(&"-la".to_string()), "original args must be preserved" ); assert!( args.contains(&"/tmp".to_string()), "original args must be preserved" ); } #[test] fn bubblewrap_wrap_command_binds_required_paths() { let sandbox = BubblewrapSandbox; let mut cmd = Command::new("echo"); sandbox.wrap_command(&mut cmd).unwrap(); let args: Vec = cmd .get_args() .map(|s| s.to_string_lossy().to_string()) .collect(); assert!( args.contains(&"--ro-bind".to_string()), "must include read-only bind for /usr" ); assert!( args.contains(&"--dev".to_string()), "must include /dev mount" ); assert!( args.contains(&"--proc".to_string()), "must include /proc mount" ); } }