//! The `msudo` command launches Windows processes with elevated identities. use std::ffi::{OsStr, OsString}; use std::fmt::Write as _; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; use common::{ CliError, CommonArgs, ExitCode, RenderMode, parse_color_choice, parse_format_choice, print_json, print_quick_help_error, print_structured, print_text, }; use lexopt::prelude::{Long, Short, Value as ArgValue}; use serde::{Deserialize, Serialize}; use windowsupport::{ LaunchIdentity, LaunchRequest, LaunchResult, PrivilegeMode, ProcessPriority, ShowWindowMode, TokenIntegrity, TokenStatus, }; const HELP: &str = "\ Launch Windows commands as admin, SYSTEM, or TrustedInstaller with Mercury-friendly output. Windows only. This tool is intentionally high risk. `--user system` and `--user trustedinstaller` require `--dangerous`. Usage: msudo [OPTIONS] [--] msudo run [OPTIONS] [--] msudo status [OPTIONS] msudo [OPTIONS] --shell Options: --format Structured output format: text, json, toon --json Shortcut for --format json --toon Shortcut for --format toon --color Control ANSI color output: auto, never --quiet Suppress non-essential status output --user Target identity: current-process, current-user, admin, system, trustedinstaller --dangerous Required for system or trustedinstaller launches --shell Shell preset: cmd, powershell, pwsh, wsl, git-bash, mingw, msys2, cygwin, yori, tcc, nu --integrity Integrity: untrusted, low, medium, medium-plus, high, system --privileges Privilege policy: default, enable-all, disable-all --priority Priority: idle, below-normal, normal, above-normal, high, realtime --show-window Window mode: default, hidden, normal, minimized, maximized --session Target session id for duplicated tokens --current-directory Working directory for the launched process --same-console Reuse the current console in the foreground and wait for the child --new-window Force a new window for the launched process --wait Wait for the launched process and return its exit code -h, --help Show this help text -V, --version Show the command version Examples: msudo status --json | ConvertFrom-Json msudo --user admin -- cmd /d /c whoami msudo --user system --dangerous -- cmd /d /c whoami msudo --same-console --user system --dangerous --shell powershell msudo --shell pwsh "; const STATUS_HELP: &str = "\ Show the current token state, runas availability, and shell preset resolution. Usage: msudo status [OPTIONS] Options: --format Structured output format: text, json, toon --json Shortcut for --format json --toon Shortcut for --format toon --quiet Suppress shell inventory in text output --color Control ANSI color output: auto, never -h, --help Show this help text -V, --version Show the command version Examples: msudo status --json | ConvertFrom-Json "; const RUN_HELP: &str = "\ Run a command or shell with the requested elevated identity. Usage: msudo run [OPTIONS] [--] msudo [OPTIONS] [--] msudo [OPTIONS] --shell Options: --format Structured output format: text, json, toon --json Emit JSON launch results instead of compact text output --toon Shortcut for --format toon --quiet Suppress non-essential text output --color Control ANSI color output: auto, never --user Target identity: current-process, current-user, admin, system, trustedinstaller --dangerous Required for system or trustedinstaller launches --shell Shell preset: cmd, powershell, pwsh, wsl, git-bash, mingw, msys2, cygwin, yori, tcc, nu --integrity Integrity: untrusted, low, medium, medium-plus, high, system --privileges Privilege policy: default, enable-all, disable-all --priority Priority: idle, below-normal, normal, above-normal, high, realtime --show-window Window mode: default, hidden, normal, minimized, maximized --session Target session id for duplicated tokens --current-directory Working directory for the launched process --same-console Reuse the current console in the foreground and wait for the child --new-window Force a new window for the launched process --wait Wait for the launched process and return its exit code -h, --help Show this help text -V, --version Show the command version Examples: msudo --user admin -- cmd /d /c whoami msudo --user system --dangerous -- cmd /d /c whoami msudo --same-console --user system --dangerous --shell powershell msudo --shell pwsh Interactive shells open in a new window by default; use --same-console to keep them in the current terminal and wait in the foreground. "; const RELAY_SUBCOMMAND: &str = "__relay"; static EXIT_CODE_PATH_COUNTER: AtomicU64 = AtomicU64::new(0); #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ParseOutcome { Help, Version, Run, } #[derive(Debug, Clone)] struct Cli { common: CommonArgs, command: CommandMode, } #[derive(Debug, Clone)] enum CommandMode { Help(&'static str), Status, Run(RunCli), Relay(RelayCli), } #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] struct RunCli { user: LaunchIdentity, dangerous: bool, shell: Option, integrity: Option, privileges: PrivilegeMode, priority: ProcessPriority, show_window: ShowWindowMode, session: Option, current_directory: Option, same_console: bool, new_window: bool, wait: bool, command: Vec, } #[derive(Debug, Clone)] struct RelayCli { run: RunCli, exit_code_path: PathBuf, stdout_path: Option, stderr_path: Option, console_pid: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] struct RelayExitStatus { ok: bool, exit_code: Option, error: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] enum ShellPreset { #[serde(rename = "cmd")] Cmd, #[serde(rename = "powershell")] PowerShell, #[serde(rename = "pwsh")] Pwsh, #[serde(rename = "wsl")] Wsl, #[serde(rename = "git-bash")] GitBash, #[serde(rename = "mingw")] Mingw, #[serde(rename = "msys2")] Msys2, #[serde(rename = "cygwin")] Cygwin, #[serde(rename = "yori")] Yori, #[serde(rename = "tcc")] Tcc, #[serde(rename = "nu")] Nu, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] enum ShellResolutionSource { KnownLocation, Path, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] struct ShellStatus { preset: ShellPreset, available: bool, executable: Option, resolution_source: Option, error: Option, } #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)] struct StatusReport { ok: bool, host: String, supports_runas: bool, is_elevated: bool, is_admin_member: bool, integrity: TokenIntegrity, current_user: Option, session_id: Option, active_session_id: Option, can_admin: bool, can_current_user: bool, can_system: bool, can_trustedinstaller: bool, trustedinstaller_installed: bool, trustedinstaller_running: bool, shells: Vec, } #[derive(Debug)] struct RelayLaunch { arguments: Vec, exit_path: PathBuf, stdout_path: Option, stderr_path: Option, } /// Parses CLI arguments and returns a process exit code. #[must_use] pub fn main_entry() -> i32 { match parse_cli_from(std::env::args_os()) { Ok((ParseOutcome::Help, cli)) => { if let CommandMode::Help(help) = cli.command { print!("{help}"); } else { print!("{HELP}"); } ExitCode::Success.as_i32() } Ok((ParseOutcome::Version, _)) => { println!("msudo {}", env!("CARGO_PKG_VERSION")); ExitCode::Success.as_i32() } Ok((ParseOutcome::Run, cli)) => match run(&cli) { Ok(code) => code, Err(error) => { print_quick_help_error(&error, HELP); error.exit_code().as_i32() } }, Err(error) => { print_quick_help_error(&error, HELP); error.exit_code().as_i32() } } } fn run(cli: &Cli) -> Result { let provider = WindowsSudoProvider; run_with_provider(cli, &provider) } fn run_with_provider(cli: &Cli, provider: &dyn SudoProvider) -> Result { match &cli.command { CommandMode::Help(help) => { print!("{help}"); Ok(ExitCode::Success.as_i32()) } CommandMode::Status => run_status(cli.common, provider), CommandMode::Run(run_cli) => run_launch(cli.common.render_mode(), run_cli, provider), CommandMode::Relay(relay) => run_relay(relay, provider), } } fn run_status(common: CommonArgs, provider: &dyn SudoProvider) -> Result { let status = provider.status()?; let report = build_status_report(status); match common.render_mode() { RenderMode::Json => print_json(&report)?, RenderMode::Toon => print_structured(&report, RenderMode::Toon)?, RenderMode::Text => print_text(render_status_text(&report, common.quiet))?, } Ok(ExitCode::Success.as_i32()) } fn run_launch( render_mode: RenderMode, run_cli: &RunCli, provider: &dyn SudoProvider, ) -> Result { validate_run_cli(run_cli)?; let status = provider.status()?; let prepared = prepare_launch(run_cli)?; if !status.is_elevated && launch_requires_elevated_relay(run_cli) { let relay = build_relay_arguments(run_cli); let result = provider.elevate_current_process( &relay.arguments, effective_wait(run_cli), run_cli.show_window, )?; if effective_wait(run_cli) { let exit_code = read_relay_exit_code(&relay.exit_path)?; replay_relay_output(relay.stdout_path.as_deref(), relay.stderr_path.as_deref())?; return Ok(exit_code); } return Ok(exit_code_from_launch(result)); } let result = provider.launch_request(&prepared)?; if matches!(render_mode, RenderMode::Json) { print_json(&result)?; } Ok(exit_code_from_launch(result)) } const fn launch_requires_elevated_relay(run_cli: &RunCli) -> bool { match run_cli.user { LaunchIdentity::CurrentProcess => false, LaunchIdentity::CurrentUser => { run_cli.session.is_some() || run_cli.integrity.is_some() || !matches!(run_cli.privileges, PrivilegeMode::Default) } LaunchIdentity::Admin | LaunchIdentity::System | LaunchIdentity::TrustedInstaller => true, } } fn run_relay(relay: &RelayCli, provider: &dyn SudoProvider) -> Result { validate_run_cli(&relay.run)?; if relay.run.same_console { let console_pid = relay.console_pid.ok_or_else(|| { CliError::usage("internal same-console relay requires --console-pid ") })?; provider.attach_parent_console(console_pid)?; } let mut prepared = prepare_launch(&relay.run)?; prepared.stdout_path = relay .stdout_path .as_ref() .map(|path| path.to_string_lossy().into_owned()); prepared.stderr_path = relay .stderr_path .as_ref() .map(|path| path.to_string_lossy().into_owned()); match provider.launch_request(&prepared) { Ok(result) => { let exit_code = exit_code_from_launch(result); write_relay_exit_status( &relay.exit_code_path, &RelayExitStatus { ok: true, exit_code: Some(exit_code), error: None, }, )?; Ok(exit_code) } Err(error) => { write_relay_exit_status( &relay.exit_code_path, &RelayExitStatus { ok: false, exit_code: None, error: Some(error.to_string()), }, )?; Err(error) } } } fn exit_code_from_launch(result: LaunchResult) -> i32 { result .exit_code .unwrap_or_else(|| ExitCode::Success.as_i32()) } fn build_status_report(status: TokenStatus) -> StatusReport { StatusReport { ok: true, host: std::env::var("COMPUTERNAME").unwrap_or_else(|_| "localhost".to_string()), supports_runas: true, is_elevated: status.is_elevated, is_admin_member: status.is_admin_member, integrity: status.integrity, current_user: status.current_user, session_id: status.session_id, active_session_id: status.active_session_id, can_admin: status.can_admin, can_current_user: status.can_current_user, can_system: status.can_system, can_trustedinstaller: status.can_trustedinstaller, trustedinstaller_installed: status.trustedinstaller_installed, trustedinstaller_running: status.trustedinstaller_running, shells: ShellPreset::all() .into_iter() .map(resolve_shell_status) .collect(), } } fn render_status_text(report: &StatusReport, quiet: bool) -> String { let mut rendered = String::new(); let _ = writeln!( rendered, "host={} elevated={} admin_member={} integrity={:?}", report.host, report.is_elevated, report.is_admin_member, report.integrity ); let _ = writeln!( rendered, "runas={} current_user={} system={} trustedinstaller={} session={} active_session={}", report.supports_runas, report.can_current_user, report.can_system, report.can_trustedinstaller, format_optional_u32(report.session_id), format_optional_u32(report.active_session_id) ); if !quiet { for shell in &report.shells { let _ = writeln!( rendered, "shell={} available={} executable={} source={}", shell.preset.name(), shell.available, shell.executable.as_deref().unwrap_or("-"), shell .resolution_source .map_or("-", shell_resolution_source_name), ); } } rendered } fn validate_run_cli(run_cli: &RunCli) -> Result<(), CliError> { if matches!( run_cli.user, LaunchIdentity::System | LaunchIdentity::TrustedInstaller ) && !run_cli.dangerous { return Err(CliError::usage( "--user system and --user trustedinstaller require --dangerous", )); } if run_cli.command.is_empty() && run_cli.shell.is_none() { return Err(CliError::usage( "provide a command after -- or select --shell ", )); } if run_cli.same_console && run_cli.new_window { return Err(CliError::usage( "--same-console cannot be combined with --new-window", )); } if matches!(run_cli.user, LaunchIdentity::CurrentProcess) && (run_cli.integrity.is_some() || !matches!(run_cli.privileges, PrivilegeMode::Default) || run_cli.session.is_some()) { return Err(CliError::usage( "--user current-process cannot be combined with token shaping options", )); } Ok(()) } fn prepare_launch(run_cli: &RunCli) -> Result { let interactive_shell = is_interactive_shell(run_cli); let current_directory = effective_current_directory(run_cli); let prepared_command = if run_cli.command.is_empty() { prepare_interactive_shell(run_cli.shell.expect("shell checked in validate"))? } else if let Some(preset) = run_cli.shell { prepare_shell_wrapped_command(preset, &run_cli.command)? } else { prepare_direct_command( &run_cli.command[0], &run_cli.command[1..], run_cli.user, std::env::var_os("PATH").as_deref(), std::env::var_os("PATHEXT").as_deref(), )? }; let new_window = effective_new_window(run_cli, interactive_shell); Ok(LaunchRequest { program: prepared_command.program, args: prepared_command.args, current_directory, stdout_path: None, stderr_path: None, identity: run_cli.user, privileges: run_cli.privileges, integrity: run_cli.integrity, priority: run_cli.priority, show_window: run_cli.show_window, session: run_cli.session, same_console: run_cli.same_console, new_window, wait: effective_wait(run_cli), }) } fn is_interactive_shell(run_cli: &RunCli) -> bool { run_cli.command.is_empty() && run_cli.shell.is_some() } const fn effective_wait(run_cli: &RunCli) -> bool { run_cli.wait || run_cli.same_console } fn effective_current_directory(run_cli: &RunCli) -> Option { run_cli.current_directory.clone().or_else(|| { std::env::current_dir() .ok() .map(|path| path.to_string_lossy().into_owned()) }) } const fn effective_new_window(run_cli: &RunCli, interactive_shell: bool) -> bool { if run_cli.same_console { false } else { run_cli.new_window || interactive_shell } } fn build_relay_arguments(run_cli: &RunCli) -> RelayLaunch { let exit_path = unique_exit_code_path(); let (stdout_path, stderr_path) = if effective_wait(run_cli) && !is_interactive_shell(run_cli) { (Some(unique_exit_code_path()), Some(unique_exit_code_path())) } else { (None, None) }; let mut arguments = vec![ RELAY_SUBCOMMAND.to_string(), "--user".to_string(), launch_identity_name(run_cli.user).to_string(), "--exit-code-path".to_string(), exit_path.display().to_string(), ]; if run_cli.dangerous { arguments.push("--dangerous".to_string()); } if let Some(shell) = run_cli.shell { arguments.push("--shell".to_string()); arguments.push(shell.name().to_string()); } if let Some(integrity) = run_cli.integrity { arguments.push("--integrity".to_string()); arguments.push(token_integrity_name(integrity).to_string()); } if !matches!(run_cli.privileges, PrivilegeMode::Default) { arguments.push("--privileges".to_string()); arguments.push(privilege_mode_name(run_cli.privileges).to_string()); } if !matches!(run_cli.priority, ProcessPriority::Normal) { arguments.push("--priority".to_string()); arguments.push(priority_name(run_cli.priority).to_string()); } if !matches!(run_cli.show_window, ShowWindowMode::Default) { arguments.push("--show-window".to_string()); arguments.push(show_window_name(run_cli.show_window).to_string()); } if let Some(session) = run_cli.session { arguments.push("--session".to_string()); arguments.push(session.to_string()); } if let Some(current_directory) = effective_current_directory(run_cli) { arguments.push("--current-directory".to_string()); arguments.push(current_directory); } if run_cli.same_console { arguments.push("--same-console".to_string()); arguments.push("--console-pid".to_string()); arguments.push(std::process::id().to_string()); } if let Some(path) = &stdout_path { arguments.push("--stdout-path".to_string()); arguments.push(path.display().to_string()); } if let Some(path) = &stderr_path { arguments.push("--stderr-path".to_string()); arguments.push(path.display().to_string()); } if run_cli.new_window { arguments.push("--new-window".to_string()); } if effective_wait(run_cli) { arguments.push("--wait".to_string()); } if !run_cli.command.is_empty() { arguments.push("--".to_string()); arguments.extend(run_cli.command.iter().cloned()); } RelayLaunch { arguments, exit_path, stdout_path, stderr_path, } } fn unique_exit_code_path() -> PathBuf { let temp_dir = std::env::temp_dir(); for _ in 0..32 { let unique = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |value| value.as_nanos()); let counter = EXIT_CODE_PATH_COUNTER.fetch_add(1, Ordering::Relaxed); let candidate = temp_dir.join(format!( "msudo-exit-{}-{unique}-{counter}.json", std::process::id() )); if !candidate.exists() { return candidate; } } let counter = EXIT_CODE_PATH_COUNTER.fetch_add(1, Ordering::Relaxed); temp_dir.join(format!( "msudo-exit-fallback-{}-{counter}.json", std::process::id() )) } fn write_relay_exit_status(path: &Path, status: &RelayExitStatus) -> Result<(), CliError> { let payload = serde_json::to_string(status).map_err(|error| { CliError::runtime(format!("failed to encode relay exit status: {error}")) })?; let mut file = fs::OpenOptions::new() .write(true) .create_new(true) .open(path) .map_err(|error| { CliError::runtime(format!( "failed to create relay exit status at {}: {error}", path.display() )) })?; file.write_all(payload.as_bytes()).map_err(|error| { CliError::runtime(format!( "failed to write relay exit status to {}: {error}", path.display() )) }) } fn read_relay_exit_code(path: &Path) -> Result { let deadline = Instant::now() + Duration::from_secs(2); let content = loop { match fs::read_to_string(path) { Ok(content) => break content, Err(error) if Instant::now() < deadline && matches!( error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied ) => { std::thread::sleep(Duration::from_millis(25)); } Err(error) => { return Err(CliError::runtime(format!( "failed to read relay exit code from {}: {error}", path.display() ))); } } }; let _ = fs::remove_file(path); if let Ok(status) = serde_json::from_str::(content.trim()) { if status.ok { return status.exit_code.ok_or_else(|| { CliError::runtime("relay exit status was missing an exit code".to_string()) }); } return Err(CliError::runtime(status.error.unwrap_or_else(|| { "privileged relay failed without an error message".to_string() }))); } content.trim().parse::().map_err(|error| { CliError::runtime(format!( "failed to parse relay exit status from {}: {error}", path.display() )) }) } fn replay_relay_output( stdout_path: Option<&Path>, stderr_path: Option<&Path>, ) -> Result<(), CliError> { if let Some(path) = stdout_path { replay_one_relay_output(path, false)?; } if let Some(path) = stderr_path { replay_one_relay_output(path, true)?; } Ok(()) } fn replay_one_relay_output(path: &Path, stderr: bool) -> Result<(), CliError> { let bytes = fs::read(path).map_err(|error| { CliError::runtime(format!( "failed to read relay output from {}: {error}", path.display() )) })?; let _ = fs::remove_file(path); if stderr { std::io::stderr().write_all(&bytes).map_err(|error| { CliError::runtime(format!("failed to replay relay stderr: {error}")) })?; std::io::stderr() .flush() .map_err(|error| CliError::runtime(format!("failed to flush relay stderr: {error}")))?; } else { std::io::stdout().write_all(&bytes).map_err(|error| { CliError::runtime(format!("failed to replay relay stdout: {error}")) })?; std::io::stdout() .flush() .map_err(|error| CliError::runtime(format!("failed to flush relay stdout: {error}")))?; } Ok(()) } fn prepare_interactive_shell(shell: ShellPreset) -> Result { let resolution = resolve_shell(shell)?; Ok(PreparedCommand { program: resolution.executable, args: resolution.default_args, }) } fn prepare_shell_wrapped_command( shell: ShellPreset, command: &[String], ) -> Result { let resolution = resolve_shell(shell)?; let shell_command = quote_for_shell(shell, command)?; let args = match shell { ShellPreset::Cmd | ShellPreset::Tcc | ShellPreset::Yori => { let mut args = resolution.default_args; args.push("/d".to_string()); args.push("/s".to_string()); args.push("/c".to_string()); args.push(shell_command); args } ShellPreset::PowerShell | ShellPreset::Pwsh => { let mut args = resolution.default_args; args.push("-Command".to_string()); args.push(shell_command); args } ShellPreset::Wsl => { let mut args = resolution.default_args; args.push("--".to_string()); args.extend(command.iter().cloned()); args } ShellPreset::GitBash | ShellPreset::Mingw | ShellPreset::Msys2 | ShellPreset::Cygwin => { let mut args = resolution.default_args; args.push("-lc".to_string()); args.push(shell_command); args } ShellPreset::Nu => { let mut args = resolution.default_args; args.push("-c".to_string()); args.push(shell_command); args } }; Ok(PreparedCommand { program: resolution.executable, args, }) } fn prepare_direct_command( program: &str, args: &[String], identity: LaunchIdentity, path: Option<&OsStr>, pathext: Option<&OsStr>, ) -> Result { let resolution = resolve_direct_command_from_path(program, path, pathext); validate_direct_command_policy(program, identity, resolution.source)?; Ok(PreparedCommand { program: resolution.executable, args: args.to_vec(), }) } fn quote_for_shell(shell: ShellPreset, values: &[String]) -> Result { match shell { ShellPreset::PowerShell | ShellPreset::Pwsh => Ok(quote_powershell_invocation(values)), ShellPreset::Cmd | ShellPreset::Tcc | ShellPreset::Yori => Ok(values .iter() .map(|value| quote_windows_style(value)) .collect::, _>>()? .join(" ")), _ => Ok(values .iter() .map(|value| quote_posix_style(value)) .collect::>() .join(" ")), } } fn quote_powershell_invocation(values: &[String]) -> String { let Some((program, args)) = values.split_first() else { return "& ''".to_string(); }; let program = quote_powershell_string(program); if args.is_empty() { return format!("& {program}"); } let args = args .iter() .map(|value| quote_powershell_string(value)) .collect::>() .join(", "); format!("& {program} @({args})") } fn quote_powershell_string(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } fn quote_windows_style(value: &str) -> Result { if let Some(character) = value .chars() .find(|character| matches!(character, '%' | '`' | '\r' | '\n')) { return Err(CliError::usage(format!( "unsafe shell metacharacter {character:?} is not allowed with cmd-style shells" ))); } if value.is_empty() { return Ok("\"\"".to_string()); } if !value.contains([' ', '\t', '"', '&', '|', '^', '>', '<']) { return Ok(value.to_string()); } Ok(format!("\"{}\"", value.replace('"', "\\\""))) } fn quote_posix_style(value: &str) -> String { if value.is_empty() { return "''".to_string(); } if !value.contains([ ' ', '\t', '\'', '"', '$', '|', '&', ';', '>', '<', '`', '\r', '\n', '%', ]) { return value.to_string(); } format!("'{}'", value.replace('\'', "'\"'\"'")) } fn resolve_shell_status(preset: ShellPreset) -> ShellStatus { match preset.try_resolve() { Ok(resolution) => ShellStatus { preset, available: true, executable: Some(resolution.executable), resolution_source: Some(resolution.source), error: None, }, Err(error) => ShellStatus { preset, available: false, executable: None, resolution_source: None, error: Some(error), }, } } #[derive(Debug, Clone)] struct PreparedCommand { program: String, args: Vec, } #[derive(Debug, Clone)] struct ResolvedShell { executable: String, default_args: Vec, source: ShellResolutionSource, } trait SudoProvider { fn status(&self) -> Result; fn elevate_current_process( &self, arguments: &[String], wait: bool, show_window: ShowWindowMode, ) -> Result; fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError>; fn launch_request(&self, request: &LaunchRequest) -> Result; } struct WindowsSudoProvider; impl SudoProvider for WindowsSudoProvider { fn status(&self) -> Result { windowsupport::current_token_status().map_err(|error| CliError::runtime(error.to_string())) } fn elevate_current_process( &self, arguments: &[String], wait: bool, show_window: ShowWindowMode, ) -> Result { windowsupport::elevate_current_process(arguments, wait, show_window) .map_err(|error| CliError::runtime(error.to_string())) } fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError> { windowsupport::attach_parent_console(parent_pid) .map_err(|error| CliError::runtime(error.to_string())) } fn launch_request(&self, request: &LaunchRequest) -> Result { windowsupport::launch_request(request).map_err(|error| CliError::runtime(error.to_string())) } } impl ShellPreset { const fn all() -> [Self; 11] { [ Self::Cmd, Self::PowerShell, Self::Pwsh, Self::Wsl, Self::GitBash, Self::Mingw, Self::Msys2, Self::Cygwin, Self::Yori, Self::Tcc, Self::Nu, ] } const fn name(self) -> &'static str { match self { Self::Cmd => "cmd", Self::PowerShell => "powershell", Self::Pwsh => "pwsh", Self::Wsl => "wsl", Self::GitBash => "git-bash", Self::Mingw => "mingw", Self::Msys2 => "msys2", Self::Cygwin => "cygwin", Self::Yori => "yori", Self::Tcc => "tcc", Self::Nu => "nu", } } fn try_resolve(self) -> Result { resolve_shell_from_path( self, std::env::var_os("PATH").as_deref(), std::env::var_os("PATHEXT").as_deref(), ) } } fn default_shell_args(shell: ShellPreset) -> Vec { match shell { ShellPreset::PowerShell | ShellPreset::Pwsh => vec!["-NoProfile".to_string()], _ => Vec::new(), } } const fn shell_candidates(shell: ShellPreset) -> &'static [&'static str] { match shell { ShellPreset::Cmd => &["cmd.exe", "cmd"], ShellPreset::PowerShell => &["powershell.exe", "powershell"], ShellPreset::Pwsh => &["pwsh.exe", "pwsh"], ShellPreset::Wsl => &["wsl.exe", "wsl"], ShellPreset::GitBash => &["bash.exe", "bash", "git-bash.exe"], ShellPreset::Mingw | ShellPreset::Msys2 | ShellPreset::Cygwin => &[], ShellPreset::Yori => &["yori.exe", "yori"], ShellPreset::Tcc => &["tcc.exe", "tcc"], ShellPreset::Nu => &["nu.exe", "nu"], } } const fn shell_known_locations(shell: ShellPreset) -> &'static [&'static str] { match shell { ShellPreset::Cmd => &["C:\\Windows\\System32\\cmd.exe"], ShellPreset::PowerShell => { &["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"] } ShellPreset::Pwsh => &[ "C:\\Program Files\\PowerShell\\7\\pwsh.exe", "C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe", ], ShellPreset::Wsl => &["C:\\Windows\\System32\\wsl.exe"], ShellPreset::GitBash => &[ "C:\\Program Files\\Git\\usr\\bin\\bash.exe", "C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files\\Git\\git-bash.exe", ], ShellPreset::Mingw | ShellPreset::Msys2 => &["C:\\msys64\\usr\\bin\\bash.exe"], ShellPreset::Cygwin => &["C:\\cygwin64\\bin\\bash.exe"], ShellPreset::Yori => &["C:\\Program Files\\Yori\\yori.exe"], ShellPreset::Tcc => &["C:\\Program Files\\JPSoft\\TCC.exe"], ShellPreset::Nu => &[ "C:\\Program Files\\Nushell\\bin\\nu.exe", "C:\\Program Files\\nu\\bin\\nu.exe", ], } } fn shell_resolution_candidates(shell: ShellPreset) -> Vec<&'static str> { let known = shell_known_locations(shell); let path = shell_candidates(shell); let mut values = Vec::with_capacity(known.len() + path.len()); values.extend_from_slice(known); values.extend_from_slice(path); values } fn resolve_shell(shell: ShellPreset) -> Result { shell.try_resolve().map_err(CliError::runtime) } fn resolve_shell_from_path( shell: ShellPreset, path: Option<&OsStr>, pathext: Option<&OsStr>, ) -> Result { for candidate in shell_resolution_candidates(shell) { if let Some(resolution) = find_executable(candidate, path, pathext) { return Ok(ResolvedShell { executable: resolution.executable, default_args: default_shell_args(shell), source: resolution.source, }); } } Err(format!("no executable found for {}", shell.name())) } #[derive(Debug, Clone, PartialEq, Eq)] struct ResolvedExecutable { executable: String, source: ShellResolutionSource, } fn find_executable( candidate: &str, path: Option<&OsStr>, pathext: Option<&OsStr>, ) -> Option { let candidate_path = Path::new(candidate); if candidate_path.components().count() > 1 && candidate_path.exists() { return Some(ResolvedExecutable { executable: candidate_path.display().to_string(), source: ShellResolutionSource::KnownLocation, }); } find_executable_in_path(candidate, path?, pathext).map(|executable| ResolvedExecutable { executable, source: ShellResolutionSource::Path, }) } fn find_executable_in_path( candidate: &str, path: &OsStr, pathext: Option<&OsStr>, ) -> Option { let pathext = pathext.map_or_else( || vec![".exe".to_string(), ".cmd".to_string(), ".bat".to_string()], |value| { value .to_string_lossy() .split(';') .map(str::to_ascii_lowercase) .collect::>() }, ); for directory in std::env::split_paths(path) { let direct = directory.join(candidate); if direct.exists() { return Some(direct.display().to_string()); } if direct.extension().is_none() { for extension in &pathext { let path = directory.join(format!("{candidate}{extension}")); if path.exists() { return Some(path.display().to_string()); } } } } None } fn resolve_direct_command_from_path( program: &str, path: Option<&OsStr>, pathext: Option<&OsStr>, ) -> ResolvedExecutable { let program_path = Path::new(program); if program_path.components().count() > 1 { return ResolvedExecutable { executable: program.to_string(), source: ShellResolutionSource::KnownLocation, }; } for candidate in direct_command_known_locations(program) { let candidate_path = Path::new(candidate); if candidate_path.exists() { return ResolvedExecutable { executable: candidate_path.display().to_string(), source: ShellResolutionSource::KnownLocation, }; } } find_executable_in_path(program, path.unwrap_or_else(|| OsStr::new("")), pathext).map_or_else( || ResolvedExecutable { executable: program.to_string(), source: ShellResolutionSource::Path, }, |executable| ResolvedExecutable { executable, source: ShellResolutionSource::Path, }, ) } fn direct_command_known_locations(program: &str) -> &'static [&'static str] { match program.to_ascii_lowercase().as_str() { "cmd" | "cmd.exe" => shell_known_locations(ShellPreset::Cmd), "powershell" | "powershell.exe" => shell_known_locations(ShellPreset::PowerShell), "pwsh" | "pwsh.exe" => shell_known_locations(ShellPreset::Pwsh), "wsl" | "wsl.exe" => shell_known_locations(ShellPreset::Wsl), _ => &[], } } fn validate_direct_command_policy( program: &str, identity: LaunchIdentity, source: ShellResolutionSource, ) -> Result<(), CliError> { if matches!( identity, LaunchIdentity::Admin | LaunchIdentity::System | LaunchIdentity::TrustedInstaller ) && matches!(source, ShellResolutionSource::Path) { return Err(CliError::usage(format!( "{identity:?} launch refuses PATH-resolved bare command '{program}'; use an explicit path or --shell " ))); } Ok(()) } const fn shell_resolution_source_name(source: ShellResolutionSource) -> &'static str { match source { ShellResolutionSource::KnownLocation => "known-location", ShellResolutionSource::Path => "path", } } fn format_optional_u32(value: Option) -> String { value.map_or_else(|| "-".to_string(), |item| item.to_string()) } const fn launch_identity_name(identity: LaunchIdentity) -> &'static str { match identity { LaunchIdentity::CurrentProcess => "current-process", LaunchIdentity::CurrentUser => "current-user", LaunchIdentity::Admin => "admin", LaunchIdentity::System => "system", LaunchIdentity::TrustedInstaller => "trustedinstaller", } } const fn token_integrity_name(level: TokenIntegrity) -> &'static str { match level { TokenIntegrity::Untrusted => "untrusted", TokenIntegrity::Low => "low", TokenIntegrity::Medium => "medium", TokenIntegrity::MediumPlus => "medium-plus", TokenIntegrity::High => "high", TokenIntegrity::System => "system", TokenIntegrity::Unknown => "unknown", } } const fn privilege_mode_name(mode: PrivilegeMode) -> &'static str { match mode { PrivilegeMode::Default => "default", PrivilegeMode::EnableAll => "enable-all", PrivilegeMode::DisableAll => "disable-all", } } const fn priority_name(priority: ProcessPriority) -> &'static str { match priority { ProcessPriority::Idle => "idle", ProcessPriority::BelowNormal => "below-normal", ProcessPriority::Normal => "normal", ProcessPriority::AboveNormal => "above-normal", ProcessPriority::High => "high", ProcessPriority::Realtime => "realtime", } } const fn show_window_name(mode: ShowWindowMode) -> &'static str { match mode { ShowWindowMode::Default => "default", ShowWindowMode::Hidden => "hidden", ShowWindowMode::Normal => "normal", ShowWindowMode::Minimized => "minimized", ShowWindowMode::Maximized => "maximized", } } #[allow(clippy::too_many_lines)] fn parse_cli_from(args: I) -> Result<(ParseOutcome, Cli), CliError> where I: IntoIterator, T: Into, { let mut parser = lexopt::Parser::from_iter(args); let mut common = CommonArgs::default(); let mut run = RunCli { user: LaunchIdentity::Admin, dangerous: false, shell: None, integrity: None, privileges: PrivilegeMode::Default, priority: ProcessPriority::Normal, show_window: ShowWindowMode::Default, session: None, current_directory: None, same_console: false, new_window: false, wait: false, command: Vec::new(), }; let mut mode = None::; let mut relay_exit_code_path = None::; let mut relay_stdout_path = None::; let mut relay_stderr_path = None::; let mut relay_console_pid = None::; while let Some(argument) = parser .next() .map_err(|error| CliError::usage(error.to_string()))? { match argument { Long("help") | Short('h') => { let help = match mode.as_deref() { Some("status") => STATUS_HELP, Some("run" | RELAY_SUBCOMMAND) => RUN_HELP, _ => HELP, }; return Ok(( ParseOutcome::Help, Cli { common, command: CommandMode::Help(help), }, )); } Long("version") | Short('V') => { return Ok(( ParseOutcome::Version, Cli { common, command: CommandMode::Status, }, )); } Long("json") => common.set_render_mode(RenderMode::Json), Long("toon") => common.set_render_mode(RenderMode::Toon), Long("format") => { let value = parser_value_string(&mut parser, "--format")?; common.set_render_mode(parse_format_choice(&value)?); } Long("quiet") => common.quiet = true, Long("color") => { common.color = parse_color_choice(&parser_value_string(&mut parser, "--color")?)?; } Long("user") => { run.user = parse_launch_identity(&parser_value_string(&mut parser, "--user")?)?; } Long("dangerous") => run.dangerous = true, Long("shell") => { run.shell = Some(parse_shell_preset(&parser_value_string( &mut parser, "--shell", )?)?); } Long("integrity") => { run.integrity = Some(parse_token_integrity(&parser_value_string( &mut parser, "--integrity", )?)?); } Long("privileges") => { run.privileges = parse_privilege_mode(&parser_value_string(&mut parser, "--privileges")?)?; } Long("priority") => { run.priority = parse_priority(&parser_value_string(&mut parser, "--priority")?)?; } Long("show-window") => { run.show_window = parse_show_window_mode(&parser_value_string(&mut parser, "--show-window")?)?; } Long("session") => { run.session = Some(parse_u32_flag( "--session", &parser_value_string(&mut parser, "--session")?, )?); } Long("current-directory") => { run.current_directory = Some(parser_value_string(&mut parser, "--current-directory")?); } Long("same-console") => run.same_console = true, Long("new-window") => run.new_window = true, Long("wait") => run.wait = true, Long("exit-code-path") => { relay_exit_code_path = Some(PathBuf::from(parser_value_string( &mut parser, "--exit-code-path", )?)); } Long("stdout-path") => { relay_stdout_path = Some(PathBuf::from(parser_value_string( &mut parser, "--stdout-path", )?)); } Long("stderr-path") => { relay_stderr_path = Some(PathBuf::from(parser_value_string( &mut parser, "--stderr-path", )?)); } Long("console-pid") => { relay_console_pid = Some(parse_u32_flag( "--console-pid", &parser_value_string(&mut parser, "--console-pid")?, )?); } ArgValue(value) => { let token = os_to_utf8(value, "subcommand")?; if mode.is_none() && matches!(token.as_str(), "status" | "run" | RELAY_SUBCOMMAND) { mode = Some(token); continue; } run.command = collect_command_values(&mut parser, token)?; break; } _ => { return Err(CliError::usage( "unsupported argument; use --help to see available options", )); } } } let command = match mode.as_deref() { Some("status") => CommandMode::Status, Some("run") => { if run.command.is_empty() && run.shell.is_none() { return Err(CliError::usage( "provide a command after -- or select --shell ", )); } CommandMode::Run(run) } None => { if run.command.is_empty() && run.shell.is_none() { CommandMode::Help(HELP) } else { CommandMode::Run(run) } } Some(RELAY_SUBCOMMAND) => CommandMode::Relay(RelayCli { run, exit_code_path: relay_exit_code_path.ok_or_else(|| { CliError::usage("internal relay requires --exit-code-path ") })?, stdout_path: relay_stdout_path, stderr_path: relay_stderr_path, console_pid: relay_console_pid, }), Some(other) => { return Err(CliError::usage(format!( "unknown subcommand '{other}'; expected status or run" ))); } }; Ok((ParseOutcome::Run, Cli { common, command })) } fn collect_command_values( parser: &mut lexopt::Parser, first: String, ) -> Result, CliError> { let mut values = vec![first]; while let Some(argument) = parser .next() .map_err(|error| CliError::usage(error.to_string()))? { match argument { ArgValue(value) => values.push(os_to_utf8(value, "command argument")?), _ => { return Err(CliError::usage( "command arguments must appear after -- and cannot include extra flags", )); } } } Ok(values) } fn parse_launch_identity(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "admin" => Ok(LaunchIdentity::Admin), "current-process" | "self" => Ok(LaunchIdentity::CurrentProcess), "current-user" | "user" => Ok(LaunchIdentity::CurrentUser), "system" => Ok(LaunchIdentity::System), "trustedinstaller" => Ok(LaunchIdentity::TrustedInstaller), _ => Err(CliError::usage(format!( "invalid --user value '{value}'; expected current-process, current-user, admin, system, or trustedinstaller" ))), } } fn parse_shell_preset(value: &str) -> Result { match value { "cmd" => Ok(ShellPreset::Cmd), "powershell" => Ok(ShellPreset::PowerShell), "pwsh" => Ok(ShellPreset::Pwsh), "wsl" => Ok(ShellPreset::Wsl), "git-bash" => Ok(ShellPreset::GitBash), "mingw" => Ok(ShellPreset::Mingw), "msys2" => Ok(ShellPreset::Msys2), "cygwin" => Ok(ShellPreset::Cygwin), "yori" => Ok(ShellPreset::Yori), "tcc" => Ok(ShellPreset::Tcc), "nu" => Ok(ShellPreset::Nu), other => Err(CliError::usage(format!("invalid --shell value '{other}'"))), } } fn parse_token_integrity(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "untrusted" => Ok(TokenIntegrity::Untrusted), "low" => Ok(TokenIntegrity::Low), "medium" => Ok(TokenIntegrity::Medium), "medium-plus" => Ok(TokenIntegrity::MediumPlus), "high" => Ok(TokenIntegrity::High), "system" => Ok(TokenIntegrity::System), _ => Err(CliError::usage(format!( "invalid --integrity value '{value}'" ))), } } fn parse_privilege_mode(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "default" => Ok(PrivilegeMode::Default), "enable-all" => Ok(PrivilegeMode::EnableAll), "disable-all" => Ok(PrivilegeMode::DisableAll), _ => Err(CliError::usage(format!( "invalid --privileges value '{value}'" ))), } } fn parse_priority(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "idle" => Ok(ProcessPriority::Idle), "below-normal" | "belownormal" => Ok(ProcessPriority::BelowNormal), "normal" => Ok(ProcessPriority::Normal), "above-normal" | "abovenormal" => Ok(ProcessPriority::AboveNormal), "high" => Ok(ProcessPriority::High), "realtime" | "real-time" => Ok(ProcessPriority::Realtime), _ => Err(CliError::usage(format!( "invalid --priority value '{value}'" ))), } } fn parse_show_window_mode(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "default" => Ok(ShowWindowMode::Default), "hidden" | "hide" => Ok(ShowWindowMode::Hidden), "normal" | "show" => Ok(ShowWindowMode::Normal), "minimized" | "minimize" => Ok(ShowWindowMode::Minimized), "maximized" | "maximize" => Ok(ShowWindowMode::Maximized), _ => Err(CliError::usage(format!( "invalid --show-window value '{value}'" ))), } } fn parse_u32_flag(flag: &str, value: &str) -> Result { value .parse::() .map_err(|error| CliError::usage(format!("invalid {flag} value '{value}': {error}"))) } fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result { let value = parser .value() .map_err(|error| CliError::usage(error.to_string()))?; os_to_utf8(value, flag) } fn os_to_utf8(value: OsString, context: &str) -> Result { value.into_string().map_err(|invalid| { CliError::usage(format!( "{context} expects UTF-8 text, got '{}'", invalid.to_string_lossy() )) }) } #[cfg(test)] mod tests { use super::*; #[derive(Debug, Clone)] struct MockProvider { status: TokenStatus, elevate_result: LaunchResult, launch_result: LaunchResult, elevated_arguments: Vec, attached_console_pids: Vec, launched: Vec, relay_exit_code_to_write: Option, elevate_wait: Option, } impl MockProvider { fn new() -> Self { Self { status: TokenStatus { is_elevated: true, is_admin_member: true, integrity: TokenIntegrity::High, current_user: Some("TEST\\User".to_string()), session_id: Some(1), active_session_id: Some(1), can_admin: true, can_current_user: true, can_system: true, can_trustedinstaller: true, trustedinstaller_installed: true, trustedinstaller_running: false, }, elevate_result: LaunchResult { pid: Some(100), exit_code: Some(0), identity: LaunchIdentity::Admin, }, launch_result: LaunchResult { pid: Some(101), exit_code: Some(0), identity: LaunchIdentity::Admin, }, elevated_arguments: Vec::new(), attached_console_pids: Vec::new(), launched: Vec::new(), relay_exit_code_to_write: None, elevate_wait: None, } } } impl SudoProvider for std::cell::RefCell { fn status(&self) -> Result { Ok(self.borrow().status.clone()) } fn elevate_current_process( &self, arguments: &[String], wait: bool, _show_window: ShowWindowMode, ) -> Result { let mut state = self.borrow_mut(); state.elevated_arguments = arguments.to_vec(); state.elevate_wait = Some(wait); if let Some(exit_code) = state.relay_exit_code_to_write { let exit_path = arguments .windows(2) .find(|pair| pair[0] == "--exit-code-path") .map(|pair| PathBuf::from(&pair[1])) .expect("relay exit path"); fs::write(&exit_path, exit_code.to_string()) .expect("write relay exit code for test"); for relay_path in arguments .windows(2) .filter(|pair| pair[0] == "--stdout-path" || pair[0] == "--stderr-path") .map(|pair| PathBuf::from(&pair[1])) { fs::write(&relay_path, b"").expect("write relay output for test"); } } Ok(state.elevate_result) } fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError> { self.borrow_mut().attached_console_pids.push(parent_pid); Ok(()) } fn launch_request(&self, request: &LaunchRequest) -> Result { self.borrow_mut().launched.push(request.clone()); Ok(self.borrow().launch_result) } } #[test] fn parse_status_subcommand_and_json_flag() { let (outcome, cli) = parse_cli_from(["msudo", "status", "--json"]).expect("cli should parse"); assert!(matches!(outcome, ParseOutcome::Run)); assert!(cli.common.json); assert!(matches!(cli.command, CommandMode::Status)); } #[test] fn parse_default_command_mode_accepts_bare_payload() { let (_, cli) = parse_cli_from([ "msudo", "--user", "admin", "--", "cmd", "/d", "/c", "whoami", ]) .expect("cli should parse"); let CommandMode::Run(run) = cli.command else { panic!("expected run mode"); }; assert_eq!(run.command[0], "cmd"); assert_eq!(run.command[3], "whoami"); } #[test] fn parse_shell_preset_and_window_flags() { let (_, cli) = parse_cli_from([ "msudo", "--shell", "pwsh", "--new-window", "--priority", "high", ]) .expect("cli should parse"); let CommandMode::Run(run) = cli.command else { panic!("expected run mode"); }; assert_eq!(run.shell, Some(ShellPreset::Pwsh)); assert!(run.new_window); assert_eq!(run.priority, ProcessPriority::High); } #[test] fn parse_same_console_flag() { let (_, cli) = parse_cli_from(["msudo", "--same-console", "--shell", "powershell"]) .expect("cli should parse"); let CommandMode::Run(run) = cli.command else { panic!("expected run mode"); }; assert!(run.same_console); } #[test] fn parse_current_identity_user_modes() { let (_, current_user) = parse_cli_from(["msudo", "--user", "current-user", "--", "cmd"]) .expect("current user parses"); let CommandMode::Run(run) = current_user.command else { panic!("expected run mode"); }; assert_eq!(run.user, LaunchIdentity::CurrentUser); let (_, current_process) = parse_cli_from(["msudo", "--user", "current-process", "--", "cmd"]) .expect("current process parses"); let CommandMode::Run(run) = current_process.command else { panic!("expected run mode"); }; assert_eq!(run.user, LaunchIdentity::CurrentProcess); } #[test] fn validate_requires_dangerous_for_system_and_trustedinstaller() { let mut run = sample_run(); run.user = LaunchIdentity::System; assert!(validate_run_cli(&run).is_err()); run.user = LaunchIdentity::TrustedInstaller; assert!(validate_run_cli(&run).is_err()); run.dangerous = true; assert!(validate_run_cli(&run).is_ok()); } #[test] fn validate_rejects_conflicting_console_mode_flags() { let mut run = sample_run(); run.same_console = true; run.new_window = true; let error = validate_run_cli(&run).expect_err("validation should fail"); assert!( error .to_string() .contains("--same-console cannot be combined with --new-window") ); } #[test] fn status_report_contains_required_stable_fields() { let report = build_status_report(TokenStatus { is_elevated: false, is_admin_member: true, integrity: TokenIntegrity::Medium, current_user: Some("User".to_string()), session_id: Some(1), active_session_id: Some(1), can_admin: true, can_current_user: true, can_system: false, can_trustedinstaller: false, trustedinstaller_installed: true, trustedinstaller_running: false, }); let json = serde_json::to_string(&report).expect("json"); assert!(json.contains("\"ok\":true")); assert!(json.contains("\"supports_runas\":true")); assert!(json.contains("\"host\":")); assert!(json.contains("\"is_elevated\":false")); } #[test] fn render_status_text_uses_cli_shell_names() { let rendered = render_status_text( &StatusReport { ok: true, host: "localhost".to_string(), supports_runas: true, is_elevated: true, is_admin_member: true, integrity: TokenIntegrity::High, current_user: Some("User".to_string()), session_id: Some(1), active_session_id: Some(1), can_admin: true, can_current_user: true, can_system: true, can_trustedinstaller: true, trustedinstaller_installed: true, trustedinstaller_running: false, shells: vec![ShellStatus { preset: ShellPreset::GitBash, available: true, executable: Some("C:\\Program Files\\Git\\usr\\bin\\bash.exe".to_string()), resolution_source: Some(ShellResolutionSource::KnownLocation), error: None, }], }, false, ); assert!(rendered.contains("shell=git-bash")); assert!(!rendered.contains("shell=GitBash")); } #[test] fn quiet_status_text_omits_shell_inventory() { let rendered = render_status_text( &StatusReport { ok: true, host: "localhost".to_string(), supports_runas: true, is_elevated: true, is_admin_member: true, integrity: TokenIntegrity::High, current_user: Some("User".to_string()), session_id: Some(1), active_session_id: Some(1), can_admin: true, can_current_user: true, can_system: true, can_trustedinstaller: true, trustedinstaller_installed: true, trustedinstaller_running: false, shells: vec![ShellStatus { preset: ShellPreset::Pwsh, available: true, executable: Some("pwsh.exe".to_string()), resolution_source: Some(ShellResolutionSource::Path), error: None, }], }, true, ); assert!(!rendered.contains("shell=")); } #[test] fn shell_preset_json_names_match_cli_names() { for preset in ShellPreset::all() { assert_eq!( serde_json::to_string(&preset).expect("preset json"), format!("\"{}\"", preset.name()) ); } } #[test] fn shell_wrapping_for_pwsh_and_cmd_is_stable() { let pwsh = prepare_shell_wrapped_command( ShellPreset::Pwsh, &[ "git".to_string(), "status".to_string(), "--short".to_string(), ], ) .expect("pwsh shell command"); let normalized = pwsh.program.to_ascii_lowercase(); assert!( shell_resolution_candidates(ShellPreset::Pwsh) .iter() .any(|candidate| { normalized.ends_with(&candidate.to_ascii_lowercase()) }) ); assert!(pwsh.args.contains(&"-Command".to_string())); assert_eq!( pwsh.args.last().expect("pwsh command"), "& 'git' @('status', '--short')" ); let cmd = prepare_shell_wrapped_command( ShellPreset::Cmd, &["tool.exe".to_string(), "two words".to_string()], ) .expect("cmd shell command"); assert!(cmd.args.contains(&"/c".to_string())); assert!(cmd.args.last().expect("last arg").contains("\"two words\"")); } #[test] fn shell_wrapping_rejects_or_quotes_shell_metacharacters() { let cmd = prepare_shell_wrapped_command( ShellPreset::Cmd, &["tool.exe".to_string(), "%PATH%".to_string()], ) .expect_err("cmd percent expansion should be rejected"); assert!(cmd.to_string().contains("unsafe shell metacharacter")); let posix = quote_for_shell( ShellPreset::GitBash, &[ "tool".to_string(), ">".to_string(), "<".to_string(), "`whoami`".to_string(), "two\nlines".to_string(), "%PATH%".to_string(), ], ) .expect("posix metacharacters should be quoted"); assert_eq!(posix, "tool '>' '<' '`whoami`' 'two\nlines' '%PATH%'"); } #[test] fn non_elevated_launch_relays_through_provider() { let (_, cli) = parse_cli_from([ "msudo", "--user", "admin", "--", "cmd", "/d", "/c", "whoami", ]) .expect("cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); provider.borrow_mut().status.is_elevated = false; let code = run_with_provider(&cli, &provider).expect("run should succeed"); assert_eq!(code, ExitCode::Success.as_i32()); let elevated_arguments = provider.borrow().elevated_arguments.clone(); assert!(elevated_arguments.contains(&RELAY_SUBCOMMAND.to_string())); assert!(elevated_arguments.contains(&"--exit-code-path".to_string())); assert_eq!(provider.borrow().elevate_wait, Some(false)); } #[test] fn non_elevated_current_process_does_not_relay() { let (_, cli) = parse_cli_from([ "msudo", "--user", "current-process", "--", "cmd", "/d", "/c", "whoami", ]) .expect("cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); provider.borrow_mut().status.is_elevated = false; let code = run_with_provider(&cli, &provider).expect("run should succeed"); assert_eq!(code, ExitCode::Success.as_i32()); assert!(provider.borrow().elevated_arguments.is_empty()); assert_eq!( provider.borrow().launched[0].identity, LaunchIdentity::CurrentProcess ); } #[test] fn non_elevated_wait_uses_relay_exit_code_file() { let (_, cli) = parse_cli_from([ "msudo", "--user", "system", "--dangerous", "--wait", "--", "cmd", "/d", "/c", "exit 9", ]) .expect("cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); provider.borrow_mut().status.is_elevated = false; provider.borrow_mut().relay_exit_code_to_write = Some(9); let code = run_with_provider(&cli, &provider).expect("run should succeed"); assert_eq!(code, 9); assert_eq!(provider.borrow().elevate_wait, Some(true)); } #[test] fn non_elevated_same_console_forces_waiting_relay() { let (_, cli) = parse_cli_from([ "msudo", "--same-console", "--user", "system", "--dangerous", "--", "cmd", "/d", "/c", "exit 0", ]) .expect("cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); provider.borrow_mut().status.is_elevated = false; provider.borrow_mut().relay_exit_code_to_write = Some(0); let code = run_with_provider(&cli, &provider).expect("run should succeed"); assert_eq!(code, 0); let elevated_arguments = provider.borrow().elevated_arguments.clone(); assert!(elevated_arguments.contains(&"--same-console".to_string())); assert!(elevated_arguments.contains(&"--console-pid".to_string())); assert_eq!(provider.borrow().elevate_wait, Some(true)); } #[test] fn same_console_relay_attaches_parent_console_before_launch() { let relay_path = unique_exit_code_path(); let (_, cli) = parse_cli_from([ "msudo", "__relay", "--same-console", "--console-pid", "4242", "--exit-code-path", relay_path.to_string_lossy().as_ref(), "--user", "system", "--dangerous", "--wait", "--", "cmd", "/d", "/c", "exit 0", ]) .expect("relay cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); let code = run_with_provider(&cli, &provider).expect("relay should succeed"); assert_eq!(code, 0); assert_eq!(provider.borrow().attached_console_pids, vec![4242]); let _ = std::fs::remove_file(relay_path); } #[test] fn validate_allows_interactive_system_and_trustedinstaller_shells() { let mut run = sample_run(); run.command.clear(); run.user = LaunchIdentity::System; run.dangerous = true; assert!(validate_run_cli(&run).is_ok()); run.user = LaunchIdentity::TrustedInstaller; assert!(validate_run_cli(&run).is_ok()); } #[test] fn elevated_launch_calls_provider_directly() { let (_, cli) = parse_cli_from([ "msudo", "--user", "system", "--dangerous", "--", "cmd", "/d", "/c", "whoami", ]) .expect("cli should parse"); let provider = std::cell::RefCell::new(MockProvider::new()); let code = run_with_provider(&cli, &provider).expect("run should succeed"); assert_eq!(code, ExitCode::Success.as_i32()); assert_eq!(provider.borrow().launched.len(), 1); assert_eq!( provider.borrow().launched[0].identity, LaunchIdentity::System ); } #[test] fn prepare_launch_resolves_bare_direct_commands_from_path() { let temp_root = std::env::temp_dir().join(format!( "msudo-prepare-launch-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock after epoch") .as_nanos() )); std::fs::create_dir_all(&temp_root).expect("create temp root"); let tool = temp_root.join("tool.exe"); std::fs::write(&tool, []).expect("create tool stub"); let resolved = find_executable_in_path( "tool", temp_root.as_os_str(), Some(std::ffi::OsStr::new(".EXE")), ) .expect("tool should resolve"); let _ = std::fs::remove_file(&tool); let _ = std::fs::remove_dir(&temp_root); assert_eq!(resolved, tool.display().to_string()); } #[test] fn elevated_direct_commands_reject_path_resolved_bare_programs() { let temp_root = std::env::temp_dir().join(format!( "msudo-direct-path-policy-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock after epoch") .as_nanos() )); fs::create_dir_all(&temp_root).expect("create temp root"); let tool = temp_root.join("tool.exe"); fs::write(&tool, []).expect("create tool stub"); let error = prepare_direct_command( "tool", &["--version".to_string()], LaunchIdentity::Admin, Some(temp_root.as_os_str()), Some(std::ffi::OsStr::new(".EXE")), ) .expect_err("admin direct command should reject PATH-resolved bare programs"); let _ = fs::remove_file(&tool); let _ = fs::remove_dir(&temp_root); assert!( error .to_string() .contains("refuses PATH-resolved bare command") ); } #[test] fn current_process_direct_commands_keep_path_resolution() { let temp_root = std::env::temp_dir().join(format!( "msudo-current-process-path-policy-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock after epoch") .as_nanos() )); fs::create_dir_all(&temp_root).expect("create temp root"); let tool = temp_root.join("tool.exe"); fs::write(&tool, []).expect("create tool stub"); let prepared = prepare_direct_command( "tool", &["--version".to_string()], LaunchIdentity::CurrentProcess, Some(temp_root.as_os_str()), Some(std::ffi::OsStr::new(".EXE")), ) .expect("current-process direct command may use PATH"); let _ = fs::remove_file(&tool); let _ = fs::remove_dir(&temp_root); assert_eq!(prepared.program, tool.display().to_string()); assert_eq!(prepared.args, vec!["--version".to_string()]); } #[test] fn prepare_launch_defaults_interactive_shells_to_new_window() { let (_, cli) = parse_cli_from(["msudo", "--shell", "powershell"]).expect("cli should parse"); let CommandMode::Run(run) = cli.command else { panic!("expected run mode"); }; let prepared = prepare_launch(&run).expect("launch should prepare"); assert!(prepared.new_window); } #[test] fn prepare_launch_same_console_keeps_interactive_shell_in_foreground() { let (_, cli) = parse_cli_from(["msudo", "--same-console", "--shell", "powershell"]) .expect("cli should parse"); let CommandMode::Run(run) = cli.command else { panic!("expected run mode"); }; let prepared = prepare_launch(&run).expect("launch should prepare"); assert!(prepared.same_console); assert!(!prepared.new_window); assert!(prepared.wait); } #[test] fn prepare_launch_resolves_direct_program_to_executable_when_available() { let resolved_cmd = resolve_direct_command_from_path( "cmd", std::env::var_os("PATH").as_deref(), std::env::var_os("PATHEXT").as_deref(), ); assert!( resolved_cmd .executable .to_ascii_lowercase() .ends_with("cmd.exe") ); } #[test] fn shell_resolution_candidates_prefer_known_git_bash_locations() { let candidates = shell_resolution_candidates(ShellPreset::GitBash); assert_eq!( candidates.first().copied(), Some("C:\\Program Files\\Git\\usr\\bin\\bash.exe") ); assert!(candidates.contains(&"git-bash.exe")); } #[cfg(windows)] #[test] fn shell_resolution_prefers_known_locations_over_path_hijack_and_reports_source() { let temp_root = std::env::temp_dir().join(format!( "msudo-shell-hijack-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock after epoch") .as_nanos() )); fs::create_dir_all(&temp_root).expect("create temp root"); let fake_cmd = temp_root.join("cmd.exe"); fs::write(&fake_cmd, []).expect("create fake cmd"); let resolved = resolve_shell_from_path( ShellPreset::Cmd, Some(temp_root.as_os_str()), Some(std::ffi::OsStr::new(".EXE")), ) .expect("cmd should resolve"); let _ = fs::remove_file(&fake_cmd); let _ = fs::remove_dir(&temp_root); assert_ne!(resolved.executable, fake_cmd.display().to_string()); assert_eq!(resolved.source, ShellResolutionSource::KnownLocation); assert!( resolved .executable .to_ascii_lowercase() .ends_with("\\system32\\cmd.exe") ); } #[test] fn msys_like_presets_do_not_fall_back_to_generic_bash() { for preset in [ShellPreset::Mingw, ShellPreset::Msys2, ShellPreset::Cygwin] { let candidates = shell_resolution_candidates(preset); assert!(!candidates.contains(&"bash.exe")); assert!(!candidates.contains(&"bash")); } } #[test] fn read_relay_exit_code_parses_and_removes_temp_file() { let path = unique_exit_code_path(); assert!(!path.exists()); fs::write(&path, "17").expect("write exit code"); let code = read_relay_exit_code(&path).expect("read exit code"); assert_eq!(code, 17); assert!(!path.exists()); } #[test] fn write_relay_exit_status_refuses_replaced_path() { let path = unique_exit_code_path(); fs::write(&path, "attacker").expect("attacker replacement"); let error = write_relay_exit_status( &path, &RelayExitStatus { ok: true, exit_code: Some(0), error: None, }, ) .expect_err("relay writer should refuse existing replacement"); assert!( error .to_string() .contains("failed to create relay exit status") ); assert_eq!( fs::read_to_string(&path).expect("replacement remains"), "attacker" ); let _ = fs::remove_file(path); } #[test] fn read_relay_exit_code_surfaces_json_error_status() { let path = unique_exit_code_path(); write_relay_exit_status( &path, &RelayExitStatus { ok: false, exit_code: None, error: Some("CreateProcessWithTokenW failed with code 5".to_string()), }, ) .expect("write relay status"); let error = read_relay_exit_code(&path).expect_err("relay should surface error"); assert!( error .to_string() .contains("CreateProcessWithTokenW failed with code 5") ); assert!(!path.exists()); } fn sample_run() -> RunCli { RunCli { user: LaunchIdentity::Admin, dangerous: false, shell: Some(ShellPreset::Pwsh), integrity: None, privileges: PrivilegeMode::Default, priority: ProcessPriority::Normal, show_window: ShowWindowMode::Default, session: None, current_directory: None, same_console: false, new_window: false, wait: false, command: vec![ "cmd".to_string(), "/d".to_string(), "/c".to_string(), "whoami".to_string(), ], } } }