use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub mountpoint: PathBuf,
pub allow_paths: Vec<PathBuf>,
pub allow_read_paths: Vec<PathBuf>,
pub allow_network: bool,
pub session_id: String,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
mountpoint: PathBuf::new(),
allow_paths: Vec::new(),
allow_read_paths: Vec::new(),
allow_network: false,
session_id: String::new(),
}
}
}
pub fn generate_sandbox_profile(config: &SandboxConfig) -> String {
let mut profile = Vec::new();
let log_tag = format!("secafs-{}", config.session_id);
profile.push("(version 1)".to_string());
profile.push(format!(
r#"(deny default (with message "secafs-{}: write denied"))"#,
config.session_id
));
profile.push(format!("; Log tag: {}", log_tag));
profile.push("; Allow most operations".to_string());
profile.push("(allow process*)".to_string());
profile.push("(allow signal)".to_string());
profile.push("(allow mach*)".to_string());
profile.push("(allow sysctl*)".to_string());
profile.push("(allow system*)".to_string());
profile.push("(allow ipc*)".to_string());
profile.push("(allow pseudo-tty)".to_string());
profile.push("; Allow all file reads".to_string());
profile.push("(allow file-read*)".to_string());
profile.push("; Writable paths".to_string());
let mountpoint_str = config.mountpoint.to_string_lossy();
profile.push(format!(
r#"(allow file-write* (subpath "{}"))"#,
mountpoint_str
));
if let Some(parent) = config.mountpoint.parent() {
let run_dir_str = parent.to_string_lossy();
profile.push(format!(
r#"(allow file-write* (subpath "{}"))"#,
run_dir_str
));
}
profile.push(r#"(allow file-write* (subpath "/private/tmp"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/tmp"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/var/tmp"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/private/var/folders"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/dev"))"#.to_string());
profile.push(r#"(allow file-ioctl (subpath "/dev"))"#.to_string());
for path in &config.allow_paths {
let path_str = path.to_string_lossy();
profile.push(format!(r#"(allow file-write* (subpath "{}"))"#, path_str));
}
profile.push("; Network".to_string());
if config.allow_network {
profile.push("(allow network*)".to_string());
} else {
profile.push(r#"(allow network* (remote ip "localhost:*"))"#.to_string());
profile.push(r#"(allow network* (local ip "localhost:*"))"#.to_string());
}
profile.push("; Security and Keychain".to_string());
profile.push(r#"(allow file-write* (subpath "/private/var/db/mds"))"#.to_string());
profile.push(
r#"(allow file-write* (regex #"^/private/var/folders/[^/]+/[^/]+/C/mds/"))"#.to_string(),
);
profile
.push(r#"(allow file-write* (regex #"^/private/var/folders/[^/]+/[^/]+/T/"))"#.to_string());
if let Some(home) = dirs::home_dir() {
let home_str = home.to_string_lossy();
profile.push(format!(
r#"(allow file-write* (subpath "{}/Library"))"#,
home_str
));
}
profile.push(r#"(allow file-write* (subpath "/Library/Preferences"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/Library/Keychains"))"#.to_string());
profile.push("(allow authorization-right-obtain)".to_string());
profile.push("(allow user-preference-write)".to_string());
profile.push("(allow user-preference-read)".to_string());
profile.join("\n")
}
pub fn wrap_command_with_sandbox(
config: &SandboxConfig,
program: &Path,
args: &[String],
) -> Command {
let profile = generate_sandbox_profile(config);
let mut cmd = Command::new("sandbox-exec");
cmd.arg("-p").arg(&profile);
cmd.arg(program);
cmd.args(args);
cmd.current_dir(&config.mountpoint);
cmd.env("SECAFS", "1");
cmd.env("SECAFS_SANDBOX", "macos-sandbox");
cmd
}
pub fn generate_permissive_profile(config: &SandboxConfig) -> String {
let mut profile = Vec::new();
let log_tag = format!("secafs-{}", config.session_id);
profile.push("(version 1)".to_string());
profile.push(format!("(deny default (with message \"{}\")))", log_tag));
profile.push("(allow process*)".to_string());
profile.push("(allow file-read*)".to_string());
profile.push("(allow mach*)".to_string());
profile.push("(allow sysctl*)".to_string());
profile.push("(allow signal)".to_string());
profile.push("(allow ipc*)".to_string());
profile.push("(allow pseudo-tty)".to_string());
profile.push("(allow system*)".to_string());
let mountpoint_str = config.mountpoint.to_string_lossy();
profile.push(format!(
r#"(allow file-write* (subpath "{}"))"#,
mountpoint_str
));
profile.push(r#"(allow file-write* (subpath "/private/tmp"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/tmp"))"#.to_string());
profile.push(r#"(allow file-write* (subpath "/private/var/folders"))"#.to_string());
if config.allow_network {
profile.push("(allow network*)".to_string());
} else {
profile.push("(allow network* (remote ip \"localhost:*\"))".to_string());
profile.push("(allow network* (local ip \"localhost:*\"))".to_string());
}
profile.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_profile() {
let config = SandboxConfig {
mountpoint: PathBuf::from("/Users/test/.secafs/run/abc/mnt"),
allow_paths: vec![],
allow_read_paths: vec![],
allow_network: false,
session_id: "test123".to_string(),
};
let profile = generate_sandbox_profile(&config);
assert!(profile.contains("(version 1)"));
assert!(profile.contains("(deny default"));
assert!(profile.contains("secafs-test123: write denied"));
assert!(profile.contains("/Users/test/.secafs/run/abc/mnt"));
}
#[test]
fn test_profile_with_network() {
let config = SandboxConfig {
mountpoint: PathBuf::from("/mnt"),
allow_network: true,
..Default::default()
};
let profile = generate_sandbox_profile(&config);
assert!(profile.contains("(allow network*)"));
}
#[test]
fn test_profile_with_custom_paths() {
let config = SandboxConfig {
mountpoint: PathBuf::from("/mnt"),
allow_paths: vec![PathBuf::from("/custom/writable")],
allow_read_paths: vec![PathBuf::from("/custom/readonly")],
..Default::default()
};
let profile = generate_sandbox_profile(&config);
assert!(profile.contains("/custom/writable"));
}
}