use crate::capability::{AccessMode, CapabilitySet, NetworkMode};
use crate::error::{NonoError, Result};
use crate::sandbox::SupportInfo;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::path::Path;
use std::ptr;
use tracing::{debug, info};
extern "C" {
fn sandbox_init(profile: *const c_char, flags: u64, errorbuf: *mut *mut c_char) -> i32;
fn sandbox_free_error(errorbuf: *mut c_char);
}
extern "C" {
fn sandbox_extension_issue_file(
extension_class: *const c_char,
path: *const c_char,
flags: u32,
) -> *mut c_char;
fn sandbox_extension_consume(token: *const c_char) -> i64;
fn sandbox_extension_release(handle: i64) -> i32;
}
const EXT_CLASS_READ: &str = "com.apple.app-sandbox.read";
const EXT_CLASS_READ_WRITE: &str = "com.apple.app-sandbox.read-write";
pub fn extension_issue_file(path: &Path, access: AccessMode) -> Result<String> {
let class = match access {
AccessMode::Read => EXT_CLASS_READ,
AccessMode::Write | AccessMode::ReadWrite => EXT_CLASS_READ_WRITE,
};
let class_c = CString::new(class)
.map_err(|_| NonoError::SandboxInit("Extension class contains null byte".to_string()))?;
let path_str = path.to_str().ok_or_else(|| {
NonoError::SandboxInit(format!("Path contains non-UTF-8 bytes: {}", path.display()))
})?;
let path_c = CString::new(path_str).map_err(|_| {
NonoError::SandboxInit(format!("Path contains null byte: {}", path.display()))
})?;
let token_ptr = unsafe { sandbox_extension_issue_file(class_c.as_ptr(), path_c.as_ptr(), 0) };
if token_ptr.is_null() {
return Err(NonoError::SandboxInit(format!(
"sandbox_extension_issue_file failed for path: {}",
path.display()
)));
}
let token = unsafe { CStr::from_ptr(token_ptr) }
.to_string_lossy()
.into_owned();
unsafe { libc::free(token_ptr.cast::<libc::c_void>()) };
debug!(
"Issued extension token for {} ({:?})",
path.display(),
access
);
Ok(token)
}
pub fn extension_consume(token: &str) -> Result<i64> {
let token_c = CString::new(token)
.map_err(|_| NonoError::SandboxInit("Extension token contains null byte".to_string()))?;
let handle = unsafe { sandbox_extension_consume(token_c.as_ptr()) };
if handle < 0 {
return Err(NonoError::SandboxInit(format!(
"sandbox_extension_consume failed (handle={})",
handle
)));
}
debug!("Consumed extension token (handle={})", handle);
Ok(handle)
}
pub fn extension_release(handle: i64) -> Result<()> {
let result = unsafe { sandbox_extension_release(handle) };
if result != 0 {
return Err(NonoError::SandboxInit(format!(
"sandbox_extension_release failed for handle {}",
handle
)));
}
debug!("Released extension (handle={})", handle);
Ok(())
}
pub fn is_supported() -> bool {
true
}
pub fn support_info() -> SupportInfo {
SupportInfo {
is_supported: true,
platform: "macos",
details: "macOS Seatbelt sandbox available".to_string(),
}
}
fn collect_parent_dirs(caps: &CapabilitySet) -> std::collections::HashSet<String> {
let mut parents = std::collections::HashSet::new();
for cap in caps.fs_capabilities() {
let paths_to_walk: Vec<&std::path::Path> = if cap.original != cap.resolved {
vec![cap.resolved.as_path(), cap.original.as_path()]
} else {
vec![cap.resolved.as_path()]
};
for path in paths_to_walk {
let mut current = path.parent();
while let Some(parent) = current {
let parent_str = parent.to_string_lossy().to_string();
if parent_str == "/" || parent_str.is_empty() {
break;
}
if !parents.insert(parent_str) {
break;
}
current = parent.parent();
}
}
}
parents
}
fn path_filters_for_cap(cap: &crate::capability::FsCapability) -> Result<Vec<String>> {
let mut filters = Vec::with_capacity(2);
let resolved_str = cap.resolved.to_str().ok_or_else(|| {
NonoError::SandboxInit(format!(
"path contains non-UTF-8 bytes: {}",
cap.resolved.display()
))
})?;
let escaped_resolved = escape_path(resolved_str)?;
let kind = if cap.is_file { "literal" } else { "subpath" };
filters.push(format!("{} \"{}\"", kind, escaped_resolved));
if cap.original != cap.resolved {
if let Some(original_str) = cap.original.to_str() {
let escaped_original = escape_path(original_str)?;
filters.push(format!("{} \"{}\"", kind, escaped_original));
}
}
Ok(filters)
}
fn has_explicit_login_keychain_db_access(caps: &CapabilitySet) -> bool {
let user_login_db = std::env::var("HOME")
.ok()
.map(|home| Path::new(&home).join("Library/Keychains/login.keychain-db"));
let system_login_db = Path::new("/Library/Keychains/login.keychain-db");
let is_login_db = |path: &Path| -> bool {
if path == system_login_db {
return true;
}
if let Some(ref user_login_db) = user_login_db {
if path == user_login_db {
return true;
}
}
false
};
caps.fs_capabilities()
.iter()
.any(|cap| is_login_db(&cap.original) || is_login_db(&cap.resolved))
}
fn escape_path(path: &str) -> Result<String> {
let mut result = String::with_capacity(path.len());
for c in path.chars() {
match c {
'\\' => result.push_str("\\\\"),
'"' => result.push_str("\\\""),
c if c.is_control() => {
return Err(NonoError::SandboxInit(format!(
"path contains control character 0x{:02X}: {}",
c as u32, path
)));
}
_ => result.push(c),
}
}
Ok(result)
}
fn generate_profile(caps: &CapabilitySet) -> Result<String> {
let mut profile = String::new();
profile.push_str("(version 1)\n");
profile.push_str("(deny default)\n");
profile.push_str("(allow process-exec*)\n");
profile.push_str("(allow process-fork)\n");
profile.push_str("(allow process-info* (target self))\n");
profile.push_str("(deny process-info* (target others))\n");
profile.push_str("(allow sysctl-read)\n");
profile.push_str("(allow mach-lookup)\n");
if !has_explicit_login_keychain_db_access(caps) {
profile.push_str("(deny mach-lookup (global-name \"com.apple.SecurityServer\"))\n");
profile.push_str("(deny mach-lookup (global-name \"com.apple.securityd\"))\n");
}
profile.push_str("(allow mach-per-user-lookup)\n");
profile.push_str("(allow mach-task-name)\n");
profile.push_str("(deny mach-priv*)\n");
profile.push_str("(allow ipc-posix-shm-read-data)\n");
profile.push_str("(allow ipc-posix-shm-write-data)\n");
profile.push_str("(allow ipc-posix-shm-write-create)\n");
profile.push_str("(allow signal)\n");
profile.push_str("(allow system-socket)\n");
profile.push_str("(allow system-fsctl)\n");
profile.push_str("(allow system-info)\n");
profile.push_str("(allow file-read* (literal \"/\"))\n");
let parent_dirs = collect_parent_dirs(caps);
for parent in &parent_dirs {
let escaped = escape_path(parent)?;
profile.push_str(&format!(
"(allow file-read-metadata (literal \"{}\"))\n",
escaped
));
}
for cap in caps.fs_capabilities() {
if matches!(cap.access, AccessMode::Read | AccessMode::ReadWrite) {
for filter in path_filters_for_cap(cap)? {
profile.push_str(&format!("(allow file-map-executable ({}))\n", filter));
}
}
}
profile.push_str("(allow file-ioctl (literal \"/dev/tty\"))\n");
profile.push_str("(allow file-ioctl (regex #\"^/dev/ttys[0-9]+$\"))\n");
profile.push_str("(allow file-ioctl (regex #\"^/dev/pty[a-z][0-9a-f]+$\"))\n");
for cap in caps.fs_capabilities() {
for filter in path_filters_for_cap(cap)? {
profile.push_str(&format!("(allow file-ioctl ({}))\n", filter));
}
}
profile.push_str("(allow pseudo-tty)\n");
for cap in caps.fs_capabilities() {
match cap.access {
AccessMode::Read | AccessMode::ReadWrite => {
for filter in path_filters_for_cap(cap)? {
profile.push_str(&format!("(allow file-read* ({}))\n", filter));
}
}
AccessMode::Write => {
}
}
}
if caps.extensions_enabled() {
profile.push_str("(allow file-read* (extension \"com.apple.app-sandbox.read\"))\n");
profile.push_str("(allow file-read* (extension \"com.apple.app-sandbox.read-write\"))\n");
profile.push_str("(allow file-write* (extension \"com.apple.app-sandbox.read-write\"))\n");
}
for rule in caps.platform_rules() {
profile.push_str(rule);
profile.push('\n');
}
for cap in caps.fs_capabilities() {
match cap.access {
AccessMode::Write | AccessMode::ReadWrite => {
for filter in path_filters_for_cap(cap)? {
profile.push_str(&format!("(allow file-write* ({}))\n", filter));
}
}
AccessMode::Read => {
}
}
}
let localhost_ports = caps.localhost_ports();
match caps.network_mode() {
NetworkMode::Blocked => {
profile.push_str("(deny network*)\n");
if !localhost_ports.is_empty() {
profile.push_str(
"(allow system-socket (socket-domain AF_INET) (socket-type SOCK_STREAM))\n",
);
profile.push_str(
"(allow system-socket (socket-domain AF_INET6) (socket-type SOCK_STREAM))\n",
);
for lp in localhost_ports {
profile.push_str(&format!(
"(allow network-outbound (remote tcp \"localhost:{}\"))\n",
lp
));
}
profile.push_str("(allow network-bind)\n");
profile.push_str("(allow network-inbound)\n");
}
}
NetworkMode::ProxyOnly { port, bind_ports } => {
profile.push_str("(deny network*)\n");
profile.push_str(&format!(
"(allow network-outbound (remote tcp \"localhost:{}\"))\n",
port
));
for lp in localhost_ports {
profile.push_str(&format!(
"(allow network-outbound (remote tcp \"localhost:{}\"))\n",
lp
));
}
profile.push_str(
"(allow system-socket (socket-domain AF_INET) (socket-type SOCK_STREAM))\n",
);
profile.push_str(
"(allow system-socket (socket-domain AF_INET6) (socket-type SOCK_STREAM))\n",
);
if !bind_ports.is_empty() || !localhost_ports.is_empty() {
profile.push_str("(allow network-bind)\n");
profile.push_str("(allow network-inbound)\n");
}
}
NetworkMode::AllowAll => {
profile.push_str("(allow network-outbound)\n");
profile.push_str("(allow network-inbound)\n");
profile.push_str("(allow network-bind)\n");
}
}
if !caps.tcp_connect_ports().is_empty() || !caps.tcp_bind_ports().is_empty() {
return Err(NonoError::NetworkFilterUnsupported {
platform: "macOS".to_string(),
reason: "Seatbelt cannot filter by TCP port. Use --proxy-allow for host-level \
filtering or ProxyOnly mode instead."
.to_string(),
});
}
Ok(profile)
}
pub fn apply(caps: &CapabilitySet) -> Result<()> {
let profile = generate_profile(caps)?;
debug!("Generated Seatbelt profile:\n{}", profile);
let profile_cstr = CString::new(profile)
.map_err(|e| NonoError::SandboxInit(format!("Invalid profile string: {}", e)))?;
let mut error_buf: *mut c_char = ptr::null_mut();
let result = unsafe {
sandbox_init(
profile_cstr.as_ptr(),
0, &mut error_buf,
)
};
if result != 0 {
let error_msg = if !error_buf.is_null() {
let msg = unsafe {
std::ffi::CStr::from_ptr(error_buf)
.to_string_lossy()
.into_owned()
};
unsafe { sandbox_free_error(error_buf) };
msg
} else {
format!("sandbox_init returned error code {}", result)
};
return Err(NonoError::SandboxInit(error_msg));
}
info!("Seatbelt sandbox applied successfully");
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::capability::{CapabilitySource, FsCapability};
use std::path::PathBuf;
#[test]
fn test_generate_profile_empty() {
let caps = CapabilitySet::default();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(version 1)"));
assert!(profile.contains("(deny default)"));
assert!(profile.contains("(allow network-outbound)"));
}
#[test]
fn test_generate_profile_with_dir() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/test"),
resolved: PathBuf::from("/test"),
access: AccessMode::ReadWrite,
is_file: false,
source: CapabilitySource::User,
});
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(allow file-read* (subpath \"/test\"))"));
assert!(profile.contains("(allow file-write* (subpath \"/test\"))"));
assert!(profile.contains("(allow file-map-executable (subpath \"/test\"))"));
}
#[test]
fn test_generate_profile_with_file() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/test.txt"),
resolved: PathBuf::from("/test.txt"),
access: AccessMode::Write,
is_file: true,
source: CapabilitySource::User,
});
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("file-write*"));
assert!(profile.contains("literal \"/test.txt\""));
assert!(!profile.contains("file-map-executable"));
}
#[test]
fn test_generate_profile_no_global_file_map_executable() {
let caps = CapabilitySet::default();
let profile = generate_profile(&caps).unwrap();
assert!(!profile.contains("(allow file-map-executable)\n"));
}
#[test]
fn test_generate_profile_network_blocked() {
let caps = CapabilitySet::new().block_network();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny network*)"));
assert!(!profile.contains("(allow network-outbound)"));
}
#[test]
fn test_support_info() {
let info = support_info();
assert!(info.is_supported);
assert_eq!(info.platform, "macos");
}
#[test]
fn test_collect_parent_dirs() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/Users/test/.claude"),
resolved: PathBuf::from("/Users/test/.claude"),
access: AccessMode::ReadWrite,
is_file: false,
source: CapabilitySource::User,
});
let parents = collect_parent_dirs(&caps);
assert!(parents.contains("/Users"));
assert!(parents.contains("/Users/test"));
assert!(!parents.contains("/"));
}
#[test]
fn test_escape_path() {
assert_eq!(escape_path("/simple/path").unwrap(), "/simple/path");
assert_eq!(
escape_path("/path with\\slash").unwrap(),
"/path with\\\\slash"
);
assert_eq!(escape_path("/path\"quoted").unwrap(), "/path\\\"quoted");
}
#[test]
fn test_escape_path_rejects_control_characters() {
assert!(escape_path("/path\0with\0nulls").is_err());
assert!(escape_path("/path\nwith\nnewlines").is_err());
assert!(escape_path("/path\rwith\rreturns").is_err());
assert!(escape_path("/path\twith\ttabs").is_err());
assert!(escape_path("/path\x0bwith\x0cfeeds").is_err());
assert!(escape_path("/path\x1bwith\x1bescape").is_err());
assert!(escape_path("/path\x7fwith\x7fdel").is_err());
}
#[test]
fn test_generate_profile_with_platform_rules() {
let mut caps = CapabilitySet::new();
caps.add_platform_rule("(deny file-read-data (subpath \"/private/var/db\"))")
.unwrap();
caps.add_platform_rule("(deny file-write-unlink)").unwrap();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny file-read-data (subpath \"/private/var/db\"))"));
assert!(profile.contains("(deny file-write-unlink)"));
let platform_pos = profile
.find("(deny file-write-unlink)")
.expect("platform rule not found");
let network_pos = profile
.find("(allow network-outbound)")
.expect("network rule not found");
assert!(
platform_pos < network_pos,
"platform rules must appear before network rules"
);
}
#[test]
fn test_generate_profile_platform_rules_between_reads_and_writes() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/test"),
resolved: PathBuf::from("/test"),
access: AccessMode::ReadWrite,
is_file: false,
source: CapabilitySource::User,
});
caps.add_platform_rule("(deny file-write-unlink)").unwrap();
let profile = generate_profile(&caps).unwrap();
let read_pos = profile
.find("(allow file-read* (subpath \"/test\"))")
.expect("read rule not found");
let deny_pos = profile
.find("(deny file-write-unlink)")
.expect("deny rule not found");
let write_pos = profile
.find("(allow file-write* (subpath \"/test\"))")
.expect("write rule not found");
assert!(
read_pos < deny_pos,
"read rules must come before platform deny rules"
);
assert!(
deny_pos < write_pos,
"platform deny rules must come before write rules"
);
}
#[test]
fn test_generate_profile_platform_rules_empty() {
let caps = CapabilitySet::new();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(version 1)"));
assert!(profile.contains("(deny default)"));
}
#[test]
fn test_escape_path_injection_via_newline() {
let malicious = "/tmp/evil\n(allow file-read* (subpath \"/\"))";
assert!(escape_path(malicious).is_err());
}
#[test]
fn test_escape_path_injection_via_quote() {
let malicious = "/tmp/evil\")(allow file-read* (subpath \"/\"))(\"";
let escaped = escape_path(malicious).unwrap();
let chars: Vec<char> = escaped.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '"' {
assert!(
i > 0 && chars[i - 1] == '\\',
"unescaped quote at position {}",
i
);
}
}
}
#[test]
fn test_generate_profile_rejects_malicious_path() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/tmp/evil"),
resolved: PathBuf::from("/tmp/evil\n(allow file-read* (subpath \"/\"))"),
access: AccessMode::Read,
is_file: false,
source: CapabilitySource::User,
});
assert!(
generate_profile(&caps).is_err(),
"paths with control characters must be rejected"
);
}
#[test]
fn test_capability_source_tagging() {
let mut caps = CapabilitySet::new();
caps.add_fs(FsCapability {
original: PathBuf::from("/usr"),
resolved: PathBuf::from("/usr"),
access: AccessMode::Read,
is_file: false,
source: CapabilitySource::Group("system_read_macos".to_string()),
});
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(allow file-read* (subpath \"/usr\"))"));
}
#[test]
fn test_generate_profile_extensions_disabled_by_default() {
let caps = CapabilitySet::default();
let profile = generate_profile(&caps).unwrap();
assert!(!profile.contains("extension"));
}
#[test]
fn test_generate_profile_extensions_enabled() {
let caps = CapabilitySet::new().enable_extensions();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(allow file-read* (extension \"com.apple.app-sandbox.read\"))"));
assert!(
profile.contains("(allow file-read* (extension \"com.apple.app-sandbox.read-write\"))")
);
assert!(profile
.contains("(allow file-write* (extension \"com.apple.app-sandbox.read-write\"))"));
}
#[test]
fn test_generate_profile_extensions_before_platform_deny_rules() {
let mut caps = CapabilitySet::new().enable_extensions();
caps.add_platform_rule("(deny file-write-unlink)").unwrap();
let profile = generate_profile(&caps).unwrap();
let ext_pos = profile
.find("(allow file-read* (extension \"com.apple.app-sandbox.read\"))")
.expect("extension rule not found");
let deny_pos = profile
.find("(deny file-write-unlink)")
.expect("deny rule not found");
assert!(
ext_pos < deny_pos,
"extension rules must appear before platform deny rules"
);
}
#[test]
fn test_generate_profile_denies_keychain_mach_by_default() {
let caps = CapabilitySet::new();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny mach-lookup (global-name \"com.apple.SecurityServer\"))"));
assert!(profile.contains("(deny mach-lookup (global-name \"com.apple.securityd\"))"));
}
#[test]
fn test_generate_profile_skips_keychain_mach_deny_when_explicitly_granted() {
let mut caps = CapabilitySet::new();
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".to_string());
let keychain = PathBuf::from(home).join("Library/Keychains/login.keychain-db");
caps.add_fs(FsCapability {
original: keychain.clone(),
resolved: keychain,
access: AccessMode::Read,
is_file: true,
source: CapabilitySource::Profile,
});
let profile = generate_profile(&caps).unwrap();
assert!(!profile.contains("(deny mach-lookup (global-name \"com.apple.SecurityServer\"))"));
assert!(!profile.contains("(deny mach-lookup (global-name \"com.apple.securityd\"))"));
}
#[test]
fn test_generate_profile_keeps_keychain_mach_deny_for_non_login_keychain_paths() {
let mut caps = CapabilitySet::new();
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".to_string());
let other_keychain_file =
PathBuf::from(home).join("Library/Keychains/metadata.keychain-db");
caps.add_fs(FsCapability {
original: other_keychain_file.clone(),
resolved: other_keychain_file,
access: AccessMode::Read,
is_file: true,
source: CapabilitySource::Profile,
});
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny mach-lookup (global-name \"com.apple.SecurityServer\"))"));
assert!(profile.contains("(deny mach-lookup (global-name \"com.apple.securityd\"))"));
}
#[test]
fn test_generate_profile_proxy_only_mode() {
let caps = CapabilitySet::new().proxy_only(54321);
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny network*)"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:54321\"))"));
assert!(profile.contains("(allow system-socket)"));
assert!(!profile.contains("(allow network-outbound)\n"));
assert!(!profile.contains("(allow network-bind)"));
assert!(!profile.contains("(allow network-inbound)"));
}
#[test]
fn test_generate_profile_proxy_only_with_bind_ports() {
let caps = CapabilitySet::new().proxy_only_with_bind(54321, vec![18789, 3000]);
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny network*)"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:54321\"))"));
assert!(profile.contains("(allow system-socket)"));
assert!(profile.contains("(allow network-bind)"));
assert!(profile.contains("(allow network-inbound)"));
assert!(!profile.contains("(allow network-outbound)\n"));
}
#[test]
fn test_generate_profile_allow_all_network() {
let caps = CapabilitySet::new();
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(allow network-outbound)"));
assert!(profile.contains("(allow network-inbound)"));
assert!(profile.contains("(allow network-bind)"));
assert!(!profile.contains("(deny network*)"));
}
#[test]
fn test_generate_profile_rejects_per_port_rules() {
let caps = CapabilitySet::new().allow_tcp_connect(443);
let result = generate_profile(&caps);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
err.to_string().contains("macOS"),
"error should mention macOS: {}",
err
);
}
#[test]
fn test_generate_profile_rejects_per_port_bind_rules() {
let caps = CapabilitySet::new().allow_tcp_bind(8080);
let result = generate_profile(&caps);
assert!(result.is_err());
}
#[test]
fn test_generate_profile_blocked_with_localhost_ports() {
let caps = CapabilitySet::new()
.block_network()
.allow_localhost_port(3000)
.allow_localhost_port(5000);
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny network*)"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:3000\"))"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:5000\"))"));
assert!(profile.contains("(allow network-bind)"));
assert!(profile.contains("(allow network-inbound)"));
assert!(profile.contains("(allow system-socket"));
}
#[test]
fn test_generate_profile_proxy_with_localhost_ports() {
let caps = CapabilitySet::new()
.proxy_only(54321)
.allow_localhost_port(3000);
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(deny network*)"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:54321\"))"));
assert!(profile.contains("(allow network-outbound (remote tcp \"localhost:3000\"))"));
assert!(profile.contains("(allow network-bind)"));
assert!(profile.contains("(allow network-inbound)"));
}
#[test]
fn test_generate_profile_allow_all_with_localhost_ports() {
let caps = CapabilitySet::new().allow_localhost_port(3000);
let profile = generate_profile(&caps).unwrap();
assert!(profile.contains("(allow network-outbound)"));
assert!(profile.contains("(allow network-inbound)"));
assert!(profile.contains("(allow network-bind)"));
assert!(!profile.contains("(deny network*)"));
}
}