Expose trusted PowerShell parser selection by flavor

This commit is contained in:
Chris Bookholt
2026-07-01 05:21:41 -07:00
parent 58dfb5fb54
commit 3fae2503b3
3 changed files with 97 additions and 3 deletions

View File

@@ -4,8 +4,10 @@ pub mod is_dangerous_command;
pub mod is_safe_command;
#[cfg(windows)]
pub(crate) mod windows_safe_commands;
pub(crate) use powershell_parser::TrustedPowerShellFlavor;
#[cfg(all(test, windows))]
pub(crate) use powershell_parser::trusted_standard_pwsh_invocation_path;
#[cfg(all(test, windows))]
pub(crate) use powershell_parser::trusted_windows_powershell_invocation_path;
pub(crate) use powershell_parser::try_parse_powershell_ast_commands;
pub(crate) use powershell_parser::try_parse_powershell_ast_commands_with_trusted_flavor;

View File

@@ -55,6 +55,46 @@ pub(crate) fn try_parse_powershell_ast_commands(
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum TrustedPowerShellFlavor {
WindowsPowerShell,
PowerShell7,
}
/// Parses a script with the authoritative machine-wide parser for the requested flavor.
///
/// Unlike [`try_parse_powershell_ast_commands`], parser selection here is independent of a
/// runtime command's executable spelling. This lets callers inspect an untrusted runtime wrapper
/// without ever spawning that wrapper before approval.
pub(crate) fn try_parse_powershell_ast_commands_with_trusted_flavor(
flavor: TrustedPowerShellFlavor,
script: &str,
) -> Option<Vec<Vec<String>>> {
#[cfg(windows)]
{
let parser_executable = match flavor {
TrustedPowerShellFlavor::WindowsPowerShell => trusted_powershell_invocation_path(
TrustedPowerShellRoot::System,
WINDOWS_POWERSHELL_SUFFIX,
),
TrustedPowerShellFlavor::PowerShell7 => trusted_powershell_invocation_path(
TrustedPowerShellRoot::ProgramFiles,
WINDOWS_PWSH_SUFFIX,
),
}?;
match parse_with_powershell_ast(&parser_executable, script) {
PowershellParseOutcome::Commands(commands) => Some(commands),
PowershellParseOutcome::Unsupported | PowershellParseOutcome::Failed => None,
}
}
#[cfg(not(windows))]
{
let _ = (flavor, script);
None
}
}
/// Selects the host-side parser only when the command itself names that same trusted binary.
///
/// The parser runs before the command approval and sandbox boundaries. Bare executable names and
@@ -94,7 +134,7 @@ pub(crate) fn trusted_standard_pwsh_invocation_path() -> Option<PathBuf> {
trusted_powershell_invocation_path(TrustedPowerShellRoot::ProgramFiles, WINDOWS_PWSH_SUFFIX)
}
#[cfg(all(test, windows))]
#[cfg(windows)]
fn trusted_powershell_invocation_path(
root_kind: TrustedPowerShellRoot,
suffix: &str,
@@ -102,8 +142,7 @@ fn trusted_powershell_invocation_path(
let root = trusted_windows_root(root_kind).ok()?;
let invocation_path = join_windows_path(&root, suffix);
let invocation_path_str = invocation_path.to_str()?;
trusted_powershell_parser_executable(invocation_path_str)?;
Some(invocation_path)
trusted_powershell_parser_executable(invocation_path_str)
}
#[cfg(any(test, windows))]

View File

@@ -2,7 +2,9 @@ use std::path::PathBuf;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::command_safety::TrustedPowerShellFlavor;
use crate::command_safety::try_parse_powershell_ast_commands;
use crate::command_safety::try_parse_powershell_ast_commands_with_trusted_flavor;
use crate::shell_detect::ShellType;
use crate::shell_detect::detect_shell_type;
@@ -82,6 +84,28 @@ pub fn parse_powershell_command_into_plain_commands(
try_parse_powershell_ast_commands(executable, script)
}
/// Selects which protected machine-wide PowerShell installation parses a script.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PowerShellFlavor {
WindowsPowerShell,
PowerShell7,
}
/// Parses a PowerShell script without using the runtime wrapper as the parser executable.
///
/// The selected parser comes from the authoritative Windows System or Program Files known folder.
/// On unsupported hosts, or when the protected parser is unavailable, this fails closed.
pub fn parse_powershell_script_with_trusted_parser(
flavor: PowerShellFlavor,
script: &str,
) -> Option<Vec<Vec<String>>> {
let trusted_flavor = match flavor {
PowerShellFlavor::WindowsPowerShell => TrustedPowerShellFlavor::WindowsPowerShell,
PowerShellFlavor::PowerShell7 => TrustedPowerShellFlavor::PowerShell7,
};
try_parse_powershell_ast_commands_with_trusted_flavor(trusted_flavor, script)
}
/// This function attempts to find a powershell.exe executable on the system.
pub fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf> {
try_find_powershellish_executable_in_path(&["powershell.exe"])
@@ -152,10 +176,14 @@ fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Pat
#[cfg(test)]
mod tests {
#[cfg(windows)]
use super::PowerShellFlavor;
use super::UTF8_OUTPUT_PREFIX;
use super::extract_powershell_command;
#[cfg(windows)]
use super::parse_powershell_command_into_plain_commands;
#[cfg(windows)]
use super::parse_powershell_script_with_trusted_parser;
use super::prefix_powershell_script_with_utf8;
#[cfg(windows)]
@@ -277,4 +305,29 @@ mod tests {
]
);
}
#[cfg(windows)]
#[test]
fn parses_with_authoritative_parser_selected_independently_of_runtime_wrapper() {
assert_eq!(
parse_powershell_script_with_trusted_parser(
PowerShellFlavor::WindowsPowerShell,
"Write-Output windows",
),
Some(vec![vec![
"Write-Output".to_string(),
"windows".to_string(),
]]),
);
if crate::command_safety::trusted_standard_pwsh_invocation_path().is_some() {
assert_eq!(
parse_powershell_script_with_trusted_parser(
PowerShellFlavor::PowerShell7,
"Write-Output core",
),
Some(vec![vec!["Write-Output".to_string(), "core".to_string(),]]),
);
}
}
}