#![allow(dead_code)]
#[allow(unused_imports)]
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::Path;
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
const MACOS_SEATBELT_BASE_POLICY: &str = r#"
(version 1)
(deny default)
(allow process-exec*)
(allow process-fork*)
"#;
const MACOS_SEATBELT_NETWORK_POLICY: &str = r#"
(allow network*)
"#;
const MACOS_RESTRICTED_READ_ONLY_POLICY: &str = r#"
(version 1)
(deny default)
(allow process-exec)
(allow process-fork)
(allow file-read*)
(allow network*)
"#;
fn is_loopback_host(host: &str) -> bool {
let host_lower = host.to_lowercase();
host_lower == "localhost"
|| host == "127.0.0.1"
|| host == "::1"
|| host_lower == "localhost6"
|| host_lower == "ip6-localhost"
|| host.starts_with("127.")
|| host == "0:0:0:0:0:0:0:1"
|| host == "0:0:0:0:0:0:0:0"
|| host.contains("::1")
}
fn proxy_scheme_default_port(scheme: &str) -> u16 {
match scheme {
"https" => 443,
"socks5" | "socks5h" | "socks4" | "socks4a" => 1080,
_ => 80,
}
}
pub fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16> {
let proxy_keys = [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
];
let mut ports = BTreeSet::new();
for key in &proxy_keys {
let Some(proxy_url) = env.get(*key) else {
continue;
};
let trimmed = proxy_url.trim();
if trimmed.is_empty() {
continue;
}
let candidate = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("http://{trimmed}")
};
if let Ok(parsed) = url::Url::parse(&candidate) {
if let Some(host) = parsed.host_str() {
if is_loopback_host(host) {
let scheme = parsed.scheme().to_ascii_lowercase();
let port = parsed
.port()
.unwrap_or_else(|| proxy_scheme_default_port(&scheme));
ports.insert(port);
}
}
}
}
ports.into_iter().collect()
}
pub fn create_seatbelt_policy(policy: &super::SandboxPolicy) -> String {
match policy {
super::SandboxPolicy::DangerFullAccess => {
"(version 1)".to_string()
}
super::SandboxPolicy::ReadOnly {
file_system: _,
network_access,
} => {
let mut sbpl = String::from("(version 1)\n(deny default)\n");
sbpl.push_str("(allow process-exec)\n");
sbpl.push_str("(allow process-fork)\n");
sbpl.push_str("(allow file-read*)\n");
match network_access {
super::NetworkSandboxPolicy::FullAccess => {
sbpl.push_str("(allow network*)\n");
}
super::NetworkSandboxPolicy::NoAccess => {
}
super::NetworkSandboxPolicy::Localhost => {
sbpl.push_str("(allow network* (local ip \"127.0.0.1\"))\n");
sbpl.push_str("(allow network* (local ip \"::1\"))\n");
}
super::NetworkSandboxPolicy::Proxy => {
sbpl.push_str("(allow network*)\n");
}
}
sbpl
}
super::SandboxPolicy::ExternalSandbox { network_access } => {
let mut sbpl = String::from("(version 1)\n");
sbpl.push_str(match network_access {
super::NetworkSandboxPolicy::NoAccess => "(deny network*)\n",
_ => "",
});
sbpl
}
super::SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
} => {
let mut sbpl = String::from("(version 1)\n(deny default)\n");
sbpl.push_str("(allow process-exec)\n");
sbpl.push_str("(allow process-fork)\n");
sbpl.push_str("(allow file-read*)\n");
for root in writable_roots {
sbpl.push_str(&format!(
"(allow file-write* (subpath \"{}\"))\n",
root.display()
));
}
match network_access {
super::NetworkSandboxPolicy::FullAccess => {
sbpl.push_str("(allow network*)\n");
}
super::NetworkSandboxPolicy::NoAccess => {
}
super::NetworkSandboxPolicy::Localhost => {
sbpl.push_str("(allow network* (local ip \"127.0.0.1\"))\n");
sbpl.push_str("(allow network* (local ip \"::1\"))\n");
}
super::NetworkSandboxPolicy::Proxy => {
sbpl.push_str("(allow network*)\n");
}
}
sbpl
}
}
}
pub fn create_seatbelt_command_args_for_policies(
argv: Vec<String>,
_file_system_policy: &super::FileSystemSandboxPolicy,
network_policy: super::NetworkSandboxPolicy,
_cwd: &Path,
_enforce_managed_network: bool,
_network: Option<&()>,
) -> Vec<String> {
let policy = match network_policy {
super::NetworkSandboxPolicy::FullAccess => super::SandboxPolicy::ReadOnly {
file_system: super::FileSystemSandboxPolicy::ReadOnly,
network_access: network_policy,
},
super::NetworkSandboxPolicy::NoAccess => super::SandboxPolicy::ReadOnly {
file_system: super::FileSystemSandboxPolicy::ReadOnly,
network_access: network_policy,
},
_ => super::SandboxPolicy::ReadOnly {
file_system: super::FileSystemSandboxPolicy::ReadOnly,
network_access: network_policy,
},
};
let policy_string = create_seatbelt_policy(&policy);
let mut args = vec!["-p".to_string(), policy_string];
args.push("--".to_string());
args.extend(argv);
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seatbelt_policy_readonly() {
let policy = super::super::SandboxPolicy::ReadOnly {
file_system: super::super::FileSystemSandboxPolicy::ReadOnly,
network_access: super::super::NetworkSandboxPolicy::NoAccess,
};
let sbpl = create_seatbelt_policy(&policy);
assert!(sbpl.contains("(deny default)"));
assert!(sbpl.contains("(allow file-read*)"));
}
#[test]
fn test_proxy_loopback_ports() {
let mut env = HashMap::new();
env.insert(
"HTTP_PROXY".to_string(),
"http://localhost:8080".to_string(),
);
let ports = proxy_loopback_ports_from_env(&env);
assert!(ports.contains(&8080));
}
#[test]
fn test_seatbelt_policy_localhost_restricted() {
let policy = super::super::SandboxPolicy::ReadOnly {
file_system: super::super::FileSystemSandboxPolicy::ReadOnly,
network_access: super::super::NetworkSandboxPolicy::Localhost,
};
let sbpl = create_seatbelt_policy(&policy);
assert!(sbpl.contains("127.0.0.1"));
assert!(sbpl.contains("::1"));
assert!(!sbpl.contains("(allow network* (local ip))"));
}
#[test]
fn test_seatbelt_policy_no_network() {
let policy = super::super::SandboxPolicy::ReadOnly {
file_system: super::super::FileSystemSandboxPolicy::ReadOnly,
network_access: super::super::NetworkSandboxPolicy::NoAccess,
};
let sbpl = create_seatbelt_policy(&policy);
assert!(!sbpl.contains("(allow network"));
}
#[test]
fn test_seatbelt_policy_full_access() {
let policy = super::super::SandboxPolicy::ReadOnly {
file_system: super::super::FileSystemSandboxPolicy::ReadOnly,
network_access: super::super::NetworkSandboxPolicy::FullAccess,
};
let sbpl = create_seatbelt_policy(&policy);
assert!(sbpl.contains("(allow network*)"));
}
#[test]
fn test_seatbelt_workspace_write_with_localhost() {
let policy = super::super::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![std::path::PathBuf::from("/tmp")],
network_access: super::super::NetworkSandboxPolicy::Localhost,
};
let sbpl = create_seatbelt_policy(&policy);
assert!(sbpl.contains("/tmp"));
assert!(sbpl.contains("127.0.0.1"));
assert!(sbpl.contains("::1"));
}
#[test]
fn test_is_loopback_host() {
assert!(is_loopback_host("localhost"));
assert!(is_loopback_host("127.0.0.1"));
assert!(is_loopback_host("::1"));
assert!(is_loopback_host("127.0.0.2"));
assert!(is_loopback_host("127.0.0.255"));
assert!(is_loopback_host("0:0:0:0:0:0:0:1"));
assert!(!is_loopback_host("192.168.1.1"));
assert!(!is_loopback_host("8.8.8.8"));
}
}