use std::path::{Path, PathBuf};
pub(crate) fn generate_bwrap_profile(bwrap_path: &Path) -> String {
let abs_path = bwrap_path
.canonicalize()
.unwrap_or_else(|_| bwrap_path.to_path_buf());
format!(
r#"# Auto-generated by BoxLite for bundled bwrap.
# Load with: sudo apparmor_parser -r <this_file>
# Remove with: sudo apparmor_parser -R <this_file>
abi <abi/4.0>,
include <tunables/global>
profile boxlite_bwrap {path} flags=(attach_disconnected,mediate_deleted) {{
allow capability,
allow file rwlkm /{{**,}},
allow network,
allow unix,
allow ptrace,
allow signal,
allow mqueue,
allow io_uring,
allow userns,
allow mount,
allow umount,
allow pivot_root,
allow dbus,
# Stacked child profile for no-new-privs (bwrap uses PR_SET_NO_NEW_PRIVS)
allow pix /** -> &boxlite_bwrap//&boxlite_unpriv_bwrap,
}}
# Child profile strips capabilities within the user namespace
profile boxlite_unpriv_bwrap flags=(attach_disconnected,mediate_deleted) {{
allow file rwlkm /{{**,}},
allow network,
allow unix,
allow ptrace,
allow signal,
allow mqueue,
allow io_uring,
allow userns,
allow mount,
allow umount,
allow pivot_root,
allow dbus,
allow pix /** -> &boxlite_unpriv_bwrap,
audit deny capability,
}}
"#,
path = abs_path.display()
)
}
pub(crate) fn write_bwrap_profile(
bwrap_path: &Path,
apparmor_dir: &Path,
) -> Result<PathBuf, String> {
std::fs::create_dir_all(apparmor_dir).map_err(|e| {
format!(
"failed to create AppArmor profile directory {}: {}",
apparmor_dir.display(),
e
)
})?;
let profile_path = apparmor_dir.join("boxlite-bwrap");
let profile_content = generate_bwrap_profile(bwrap_path);
std::fs::write(&profile_path, &profile_content).map_err(|e| {
format!(
"failed to write AppArmor profile to {}: {}",
profile_path.display(),
e
)
})?;
tracing::info!(
profile_path = %profile_path.display(),
bwrap_path = %bwrap_path.display(),
"Generated AppArmor profile for bundled bwrap"
);
Ok(profile_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_bwrap_profile_contains_path() {
let profile = generate_bwrap_profile(Path::new("/opt/boxlite/bin/bwrap"));
assert!(profile.contains("/opt/boxlite/bin/bwrap"));
assert!(profile.contains("profile boxlite_bwrap"));
assert!(profile.contains("allow userns"));
}
#[test]
fn test_generate_bwrap_profile_unique_names() {
let profile = generate_bwrap_profile(Path::new("/usr/local/bin/bwrap"));
assert!(profile.contains("boxlite_bwrap"));
assert!(profile.contains("boxlite_unpriv_bwrap"));
assert!(profile.contains("audit deny capability"));
}
#[test]
fn test_generate_bwrap_profile_valid_syntax() {
let profile = generate_bwrap_profile(Path::new("/tmp/test-bwrap"));
assert!(profile.contains("abi <abi/4.0>"));
assert!(profile.contains("include <tunables/global>"));
assert!(profile.contains("flags=(attach_disconnected,mediate_deleted)"));
assert!(profile.contains("allow pix /** -> &boxlite_bwrap//&boxlite_unpriv_bwrap"));
}
}