Add option to pass through arguments to brwap, use shlex for dry-run
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"glob",
|
"glob",
|
||||||
"serde",
|
"serde",
|
||||||
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
@@ -372,6 +373,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ path = "src/main.rs"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
shlex = "1.3.0"
|
||||||
toml = "1"
|
toml = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
shlex = "1.3.0"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ pub struct Args {
|
|||||||
#[arg(long = "mask", value_name = "PATH", action = clap::ArgAction::Append)]
|
#[arg(long = "mask", value_name = "PATH", action = clap::ArgAction::Append)]
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Pass an arbitrary argument directly to bwrap (repeatable)
|
||||||
|
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
|
||||||
|
pub bwrap_args: Vec<String>,
|
||||||
|
|
||||||
/// Command and arguments to run inside the sandbox
|
/// Command and arguments to run inside the sandbox
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
pub command_and_args: Vec<OsString>,
|
pub command_and_args: Vec<OsString>,
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
|||||||
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
||||||
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
||||||
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
||||||
mask: merge_mask(args.mask, &profile.mask, &globals.mask),
|
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
|
||||||
|
bwrap_args: split_bwrap_args(merge_vecs(
|
||||||
|
args.bwrap_args,
|
||||||
|
&profile.bwrap_args,
|
||||||
|
&globals.bwrap_args,
|
||||||
|
))?,
|
||||||
command,
|
command,
|
||||||
command_args,
|
command_args,
|
||||||
})
|
})
|
||||||
@@ -99,7 +104,17 @@ fn merge_paths(
|
|||||||
Ok(config_paths.chain(cli_paths).collect())
|
Ok(config_paths.chain(cli_paths).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_mask(cli: Vec<PathBuf>, profile: &[PathBuf], globals: &[PathBuf]) -> Vec<PathBuf> {
|
fn split_bwrap_args(raw: Vec<String>) -> Result<Vec<String>, SandboxError> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for value in &raw {
|
||||||
|
let words =
|
||||||
|
shlex::split(value).ok_or_else(|| SandboxError::InvalidBwrapArg(value.clone()))?;
|
||||||
|
out.extend(words);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_vecs<T: Clone>(cli: Vec<T>, profile: &[T], globals: &[T]) -> Vec<T> {
|
||||||
globals.iter().chain(profile).cloned().chain(cli).collect()
|
globals.iter().chain(profile).cloned().chain(cli).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +219,8 @@ pub struct Options {
|
|||||||
pub ro: Vec<PathBuf>,
|
pub ro: Vec<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bwrap_args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub enum SandboxError {
|
|||||||
ConflictingMode,
|
ConflictingMode,
|
||||||
UnknownConfigKey(String),
|
UnknownConfigKey(String),
|
||||||
ConfigPathNotAbsolute(PathBuf),
|
ConfigPathNotAbsolute(PathBuf),
|
||||||
|
InvalidBwrapArg(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for SandboxError {
|
impl std::fmt::Display for SandboxError {
|
||||||
@@ -65,6 +66,9 @@ impl std::fmt::Display for SandboxError {
|
|||||||
Self::ConfigPathNotAbsolute(p) => {
|
Self::ConfigPathNotAbsolute(p) => {
|
||||||
write!(f, "config path is not absolute: {}", p.display())
|
write!(f, "config path is not absolute: {}", p.display())
|
||||||
}
|
}
|
||||||
|
Self::InvalidBwrapArg(s) => {
|
||||||
|
write!(f, "invalid quoting in --bwrap-arg: {s}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@@ -26,6 +26,7 @@ pub struct SandboxConfig {
|
|||||||
pub extra_rw: Vec<PathBuf>,
|
pub extra_rw: Vec<PathBuf>,
|
||||||
pub extra_ro: Vec<PathBuf>,
|
pub extra_ro: Vec<PathBuf>,
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
|
pub bwrap_args: Vec<String>,
|
||||||
pub command: PathBuf,
|
pub command: PathBuf,
|
||||||
pub command_args: Vec<OsString>,
|
pub command_args: Vec<OsString>,
|
||||||
pub chdir: PathBuf,
|
pub chdir: PathBuf,
|
||||||
@@ -63,9 +64,16 @@ pub fn run(config: SandboxConfig) -> Result<(), SandboxError> {
|
|||||||
let mut cmd = sandbox::build_command(&config)?;
|
let mut cmd = sandbox::build_command(&config)?;
|
||||||
|
|
||||||
if config.dry_run {
|
if config.dry_run {
|
||||||
println!("{:?}", cmd);
|
println!("{}", shell_quote_command(&cmd));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(SandboxError::Io(cmd.exec()))
|
Err(SandboxError::Io(cmd.exec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shell_quote_command(cmd: &std::process::Command) -> String {
|
||||||
|
let prog = cmd.get_program().to_string_lossy();
|
||||||
|
let args = cmd.get_args().map(|a| a.to_string_lossy());
|
||||||
|
let all: Vec<_> = std::iter::once(prog).chain(args).collect();
|
||||||
|
shlex::try_join(all.iter().map(|s| s.as_ref())).unwrap()
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
|||||||
|
|
||||||
apply_masks(&mut cmd, &config.mask);
|
apply_masks(&mut cmd, &config.mask);
|
||||||
|
|
||||||
|
cmd.args(&config.bwrap_args);
|
||||||
|
|
||||||
cmd.arg("--")
|
cmd.arg("--")
|
||||||
.arg(&config.command)
|
.arg(&config.command)
|
||||||
.args(&config.command_args);
|
.args(&config.command_args);
|
||||||
|
|||||||
@@ -245,6 +245,20 @@ fn dry_run_prints_and_exits() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dry_run_output_is_copy_pasteable_shell() {
|
||||||
|
let dry = sandbox(&["--dry-run"])
|
||||||
|
.args(["--", "bash", "-c", "echo $HOME"])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let dry_cmd = String::from_utf8_lossy(&dry.stdout).trim().to_string();
|
||||||
|
let args = shlex::split(&dry_cmd).expect("dry-run output is not valid shell");
|
||||||
|
assert_eq!(args[0], "bwrap");
|
||||||
|
assert_eq!(args[args.len() - 1], "echo $HOME");
|
||||||
|
assert_eq!(args[args.len() - 2], "-c");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blacklist_overlays_survive_tmp_bind() {
|
fn blacklist_overlays_survive_tmp_bind() {
|
||||||
fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel");
|
fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel");
|
||||||
@@ -649,6 +663,20 @@ fn mask_hides_file() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bwrap_arg_setenv_passes_through() {
|
||||||
|
let output = sandbox(&["--bwrap-arg", "--setenv MYVAR hello"])
|
||||||
|
.args(["--", "bash", "-c", "echo $MYVAR"])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
assert_eq!(
|
||||||
|
stdout, "hello",
|
||||||
|
"expected --bwrap-arg to pass --setenv through to bwrap, got: {stdout}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mask_nonexistent_path_becomes_tmpfs() {
|
fn mask_nonexistent_path_becomes_tmpfs() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user