use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use serde::Deserialize; use std::path::Path; use std::process::Command; use std::sync::LazyLock; const POWERSHELL_PARSER_SCRIPT: &str = include_str!("powershell_parser.ps1"); /// On Windows, we conservatively allow only clearly read-only PowerShell invocations /// that match a small safelist. Anything else (including direct CMD commands) is unsafe. pub fn is_safe_command_windows(command: &[String]) -> bool { if let Some(commands) = try_parse_powershell_command_sequence(command) { commands .iter() .all(|cmd| is_safe_powershell_command(cmd.as_slice())) } else { // Only PowerShell invocations are allowed on Windows for now; anything else is unsafe. false } } /// Returns each command sequence if the invocation starts with a PowerShell binary. /// For example, the tokens from `pwsh Get-ChildItem | Measure-Object` become two sequences. fn try_parse_powershell_command_sequence(command: &[String]) -> Option>> { let (exe, rest) = command.split_first()?; if is_powershell_executable(exe) { parse_powershell_invocation(exe, rest) } else { None } } /// Parse a PowerShell wrapper into the underlying command sequence when the /// script is simple enough to recover stable argv tokens. pub fn parse_powershell_command_sequence(command: &[String]) -> Option>> { try_parse_powershell_command_sequence(command) } /// Parses a PowerShell invocation into discrete command vectors, rejecting unsafe patterns. fn parse_powershell_invocation(executable: &str, args: &[String]) -> Option>> { if args.is_empty() { // Examples rejected here: "pwsh" and "powershell.exe" with no additional arguments. return None; } let mut idx = 0; while idx < args.len() { let arg = &args[idx]; let lower = arg.to_ascii_lowercase(); match lower.as_str() { "-command" | "/command" | "-c" => { let script = args.get(idx + 1)?; if idx + 2 != args.len() { // Reject if there is more than one token representing the actual command. // Examples rejected here: "pwsh -Command foo bar" and "powershell -c ls extra". return None; } return parse_powershell_script(executable, script); } _ if lower.starts_with("-command:") || lower.starts_with("/command:") => { if idx + 1 != args.len() { // Reject if there are more tokens after the command itself. // Examples rejected here: "pwsh -Command:dir C:\\" and "powershell /Command:dir C:\\" with trailing args. return None; } let script = arg.split_once(':')?.1; return parse_powershell_script(executable, script); } // Benign, no-arg flags we tolerate. "-nologo" | "-noprofile" | "-noninteractive" | "-mta" | "-sta" => { idx += 1; continue; } // Explicitly forbidden/opaque or unnecessary for read-only operations. "-encodedcommand" | "-ec" | "-file" | "/file" | "-windowstyle" | "-executionpolicy" | "-workingdirectory" => { // Examples rejected here: "pwsh -EncodedCommand ..." and "powershell -File script.ps1". return None; } // Unknown switch → bail conservatively. _ if lower.starts_with('-') => { // Examples rejected here: "pwsh -UnknownFlag" and "powershell -foo bar". return None; } // If we hit non-flag tokens, treat the remainder as a command sequence. // This happens if powershell is invoked without -Command, e.g. // ["pwsh", "-NoLogo", "git", "-c", "core.pager=cat", "status"] _ => { let script = join_arguments_as_script(&args[idx..]); return parse_powershell_script(executable, &script); } } } // Examples rejected here: "pwsh" and "powershell.exe -NoLogo" without a script. None } /// Tokenizes an inline PowerShell script and delegates to the command splitter. /// Examples of when this is called: pwsh.exe -Command '