sbexec 0.4.0

Run untrusted build commands in a least-privilege sandbox on macOS and Linux
use std::path::PathBuf;

use clap::{Parser, Subcommand};

/// sbe — Run commands in a kernel-enforced sandbox with supply chain attack
/// protection.
///
/// Wraps any command in `sandbox-exec` (macOS) or Landlock + seccomp (Linux)
/// with sensible defaults per language ecosystem (Node.js, Rust, Python,
/// Elixir, Java).
#[derive(Debug, Parser)]
#[command(name = "sbe", version, about, long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Debug, Subcommand)]
pub enum Commands {
    /// Execute a command inside the sandbox.
    Run(RunArgs),

    /// Print resolved config and the policy that would be installed
    /// (SBPL on macOS, Landlock+seccomp YAML on Linux) without executing.
    Inspect(InspectArgs),

    /// List available profiles and their defaults.
    Profiles,
}

/// Arguments shared between `run` and `inspect`.
#[derive(Debug, Parser)]
pub struct RunArgs {
    /// Use a specific profile (overrides auto-detect).
    #[arg(short = 'p', long)]
    pub profile: Option<String>,

    /// Add domain to network allowlist (repeatable).
    #[arg(short = 'n', long = "allow-domain", action = clap::ArgAction::Append)]
    pub allow_domain: Vec<String>,

    /// Remove domain from network allowlist (repeatable).
    #[arg(short = 'N', long = "deny-domain", action = clap::ArgAction::Append)]
    pub deny_domain: Vec<String>,

    /// Add writable path (repeatable).
    #[arg(short = 'w', long = "allow-write", action = clap::ArgAction::Append)]
    pub allow_write: Vec<PathBuf>,

    /// Add read-denied path (repeatable).
    #[arg(short = 'r', long = "deny-read", action = clap::ArgAction::Append)]
    pub deny_read: Vec<PathBuf>,

    /// Allow execution of binary (repeatable).
    #[arg(short = 'e', long = "allow-exec", action = clap::ArgAction::Append)]
    pub allow_exec: Vec<PathBuf>,

    /// Deny execution of binary (repeatable).
    #[arg(short = 'E', long = "deny-exec", action = clap::ArgAction::Append)]
    pub deny_exec: Vec<PathBuf>,

    /// Allow build-time downloads from a domain (enables curl/wget, adds to proxy allowlist).
    #[arg(short = 'f', long = "allow-fetch", action = clap::ArgAction::Append)]
    pub allow_fetch: Vec<String>,

    /// Disable network sandboxing entirely.
    #[arg(long)]
    pub allow_all_network: bool,

    /// Disable the proxy and allow direct TCP only on port 443.
    #[arg(long)]
    pub no_proxy: bool,

    /// Legacy alias for --allow-insecure-linux-network.
    #[arg(long, hide = true)]
    pub allow_degraded: bool,

    /// Opt in to Linux's destination-port-only network compatibility mode.
    /// This does not provide strict domain confinement against malicious code.
    #[arg(long)]
    pub allow_insecure_linux_network: bool,

    /// Trust an automatically discovered project `.sbe.yaml` to expand the
    /// sandbox. Without this flag project config may only restrict policy.
    #[arg(long)]
    pub trust_project_config: bool,

    /// Preserve one variable from the parent environment (repeatable).
    #[arg(long = "keep-env", action = clap::ArgAction::Append)]
    pub keep_env: Vec<String>,

    /// Set one child environment variable as NAME=VALUE (repeatable).
    #[arg(long = "env", action = clap::ArgAction::Append)]
    pub env: Vec<String>,

    /// Request correlated violation streaming; fails when the backend reports it unavailable.
    #[arg(long)]
    pub audit: bool,

    /// Request a private violation log; fails when correlated auditing is unavailable.
    #[arg(long)]
    pub audit_log: Option<PathBuf>,

    /// Print SBPL to stdout, do not execute.
    #[arg(long)]
    pub dry_run: bool,

    /// Use specific config file.
    #[arg(short = 'c', long)]
    pub config: Option<PathBuf>,

    /// Verbose output.
    #[arg(short = 'v', long)]
    pub verbose: bool,

    /// The command and arguments to run inside the sandbox.
    #[arg(last = true, required = true)]
    pub command: Vec<String>,
}

#[derive(Debug, Parser)]
pub struct InspectArgs {
    /// Use a specific profile (overrides auto-detect).
    #[arg(short = 'p', long)]
    pub profile: Option<String>,

    /// Add domain to network allowlist (repeatable).
    #[arg(short = 'n', long = "allow-domain", action = clap::ArgAction::Append)]
    pub allow_domain: Vec<String>,

    /// Remove domain from network allowlist (repeatable).
    #[arg(short = 'N', long = "deny-domain", action = clap::ArgAction::Append)]
    pub deny_domain: Vec<String>,

    /// Add writable path (repeatable).
    #[arg(short = 'w', long = "allow-write", action = clap::ArgAction::Append)]
    pub allow_write: Vec<PathBuf>,

    /// Add read-denied path (repeatable).
    #[arg(short = 'r', long = "deny-read", action = clap::ArgAction::Append)]
    pub deny_read: Vec<PathBuf>,

    /// Allow execution of binary (repeatable).
    #[arg(short = 'e', long = "allow-exec", action = clap::ArgAction::Append)]
    pub allow_exec: Vec<PathBuf>,

    /// Deny execution of binary (repeatable).
    #[arg(short = 'E', long = "deny-exec", action = clap::ArgAction::Append)]
    pub deny_exec: Vec<PathBuf>,

    /// Allow build-time downloads from a domain (enables curl/wget, adds to proxy allowlist).
    #[arg(short = 'f', long = "allow-fetch", action = clap::ArgAction::Append)]
    pub allow_fetch: Vec<String>,

    /// Disable network sandboxing entirely.
    #[arg(long)]
    pub allow_all_network: bool,

    /// Disable the proxy and allow direct TCP only on port 443.
    #[arg(long)]
    pub no_proxy: bool,

    /// Legacy alias for --allow-insecure-linux-network.
    #[arg(long, hide = true)]
    pub allow_degraded: bool,

    /// Opt in to Linux's destination-port-only network compatibility mode.
    #[arg(long)]
    pub allow_insecure_linux_network: bool,

    /// Trust an automatically discovered project `.sbe.yaml` to expand policy.
    #[arg(long)]
    pub trust_project_config: bool,

    /// Preserve one variable from the parent environment (repeatable).
    #[arg(long = "keep-env", action = clap::ArgAction::Append)]
    pub keep_env: Vec<String>,

    /// Set one child environment variable as NAME=VALUE (repeatable).
    #[arg(long = "env", action = clap::ArgAction::Append)]
    pub env: Vec<String>,

    /// Use specific config file.
    #[arg(short = 'c', long)]
    pub config: Option<PathBuf>,

    /// The command and arguments to inspect.
    #[arg(last = true, required = true)]
    pub command: Vec<String>,
}

impl InspectArgs {
    /// Convert inspect args into equivalent run args for profile resolution.
    pub fn as_run_args(&self) -> RunArgs {
        RunArgs {
            profile: self.profile.clone(),
            allow_domain: self.allow_domain.clone(),
            deny_domain: self.deny_domain.clone(),
            allow_write: self.allow_write.clone(),
            deny_read: self.deny_read.clone(),
            allow_exec: self.allow_exec.clone(),
            deny_exec: self.deny_exec.clone(),
            allow_fetch: self.allow_fetch.clone(),
            allow_all_network: self.allow_all_network,
            no_proxy: self.no_proxy,
            allow_degraded: self.allow_degraded,
            allow_insecure_linux_network: self.allow_insecure_linux_network,
            trust_project_config: self.trust_project_config,
            keep_env: self.keep_env.clone(),
            env: self.env.clone(),
            audit: false,
            audit_log: None,
            dry_run: true,
            config: self.config.clone(),
            verbose: false,
            command: self.command.clone(),
        }
    }
}