mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Match Windows execpolicy rules inside PowerShell wrappers
This commit is contained in:
@@ -37,6 +37,10 @@ use crate::sandboxing::SandboxPermissions;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use codex_shell_command::bash::parse_shell_lc_plain_commands;
|
||||
use codex_shell_command::bash::parse_shell_lc_single_command_prefix;
|
||||
#[cfg(windows)]
|
||||
use codex_shell_command::powershell::PowershellCommandSequenceParseMode;
|
||||
#[cfg(windows)]
|
||||
use codex_shell_command::powershell::try_parse_powershell_command_sequence;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use shlex::try_join as shlex_try_join;
|
||||
|
||||
@@ -237,7 +241,7 @@ impl ExecPolicyManager {
|
||||
req: ExecApprovalRequest<'_>,
|
||||
) -> ExecApprovalRequirement {
|
||||
let ExecApprovalRequest {
|
||||
command,
|
||||
command: original_command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
@@ -246,18 +250,32 @@ impl ExecPolicyManager {
|
||||
prefix_rule,
|
||||
} = req;
|
||||
let exec_policy = self.current();
|
||||
let (commands, used_complex_parsing) = commands_for_exec_policy(command);
|
||||
let (commands, used_complex_parsing) = commands_for_exec_policy(original_command);
|
||||
#[cfg(windows)]
|
||||
let powershell_commands = try_parse_powershell_command_sequence(
|
||||
original_command,
|
||||
PowershellCommandSequenceParseMode::ExecPolicy,
|
||||
)
|
||||
.filter(|commands| !commands.is_empty());
|
||||
// Keep heredoc prefix parsing for rule evaluation so existing
|
||||
// allow/prompt/forbidden rules still apply, but avoid auto-derived
|
||||
// amendments when only the heredoc fallback parser matched.
|
||||
let auto_amendment_allowed = !used_complex_parsing;
|
||||
let exec_policy_fallback = |cmd: &[String]| {
|
||||
let exec_policy_fallback = |parsed_command: &[String]| {
|
||||
#[cfg(windows)]
|
||||
let command_for_heuristics = if powershell_commands.is_some() {
|
||||
original_command
|
||||
} else {
|
||||
parsed_command
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
let command_for_heuristics = parsed_command;
|
||||
render_decision_for_unmatched_command(
|
||||
approval_policy,
|
||||
&permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
cmd,
|
||||
command_for_heuristics,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
)
|
||||
@@ -265,12 +283,25 @@ impl ExecPolicyManager {
|
||||
let match_options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
};
|
||||
let evaluation = exec_policy.check_multiple_with_options(
|
||||
let mut evaluation = exec_policy.check_multiple_with_options(
|
||||
commands.iter(),
|
||||
&exec_policy_fallback,
|
||||
&match_options,
|
||||
);
|
||||
|
||||
#[cfg(windows)]
|
||||
if powershell_commands.is_some() {
|
||||
for rule_match in &mut evaluation.matched_rules {
|
||||
if let RuleMatch::HeuristicsRuleMatch {
|
||||
command: matched_command,
|
||||
..
|
||||
} = rule_match
|
||||
{
|
||||
*matched_command = original_command.to_vec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let requested_amendment = derive_requested_execpolicy_amendment_from_prefix_rule(
|
||||
prefix_rule.as_ref(),
|
||||
&evaluation.matched_rules,
|
||||
@@ -282,7 +313,7 @@ impl ExecPolicyManager {
|
||||
|
||||
match evaluation.decision {
|
||||
Decision::Forbidden => ExecApprovalRequirement::Forbidden {
|
||||
reason: derive_forbidden_reason(command, &evaluation),
|
||||
reason: derive_forbidden_reason(original_command, &evaluation),
|
||||
},
|
||||
Decision::Prompt => {
|
||||
let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| {
|
||||
@@ -293,7 +324,7 @@ impl ExecPolicyManager {
|
||||
reason: reason.to_string(),
|
||||
},
|
||||
None => ExecApprovalRequirement::NeedsApproval {
|
||||
reason: derive_prompt_reason(command, &evaluation),
|
||||
reason: derive_prompt_reason(original_command, &evaluation),
|
||||
proposed_execpolicy_amendment: requested_amendment.or_else(|| {
|
||||
if auto_amendment_allowed {
|
||||
try_derive_execpolicy_amendment_for_prompt_rules(
|
||||
@@ -705,6 +736,15 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec<Vec<String>>, bool) {
|
||||
return (commands, false);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(commands) = try_parse_powershell_command_sequence(
|
||||
command,
|
||||
PowershellCommandSequenceParseMode::ExecPolicy,
|
||||
) && !commands.is_empty()
|
||||
{
|
||||
return (commands, false);
|
||||
}
|
||||
|
||||
if let Some(single_command) = parse_shell_lc_single_command_prefix(command) {
|
||||
return (vec![single_command], true);
|
||||
}
|
||||
|
||||
@@ -656,6 +656,148 @@ async fn evaluates_bash_lc_inner_commands() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn evaluates_powershell_wrapped_inner_commands_against_prefix_rules() {
|
||||
let cases = vec![
|
||||
(
|
||||
r#"prefix_rule(pattern=["git", "push"], decision="forbidden")"#.to_string(),
|
||||
vec_str(&["powershell.exe", "-Command", "git push origin main"]),
|
||||
vec_str(&["git", "push"]),
|
||||
),
|
||||
(
|
||||
r#"prefix_rule(pattern=["git", "fetch"], decision="forbidden")"#.to_string(),
|
||||
vec_str(&[
|
||||
"powershell.exe",
|
||||
"-WindowStyle",
|
||||
"Hidden",
|
||||
"-Command",
|
||||
"git fetch origin main",
|
||||
]),
|
||||
vec_str(&["git", "fetch"]),
|
||||
),
|
||||
(
|
||||
r#"prefix_rule(pattern=["Invoke-WebRequest"], decision="forbidden")"#.to_string(),
|
||||
vec_str(&[
|
||||
"powershell.exe",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
"Invoke-WebRequest https://example.com",
|
||||
]),
|
||||
vec_str(&["Invoke-WebRequest"]),
|
||||
),
|
||||
(
|
||||
r#"prefix_rule(pattern=["Remove-Item"], decision="forbidden")"#.to_string(),
|
||||
vec_str(&[
|
||||
"powershell.exe",
|
||||
"-WorkingDirectory",
|
||||
"C:\\repo",
|
||||
"-Command",
|
||||
"Remove-Item foo.txt",
|
||||
]),
|
||||
vec_str(&["Remove-Item"]),
|
||||
),
|
||||
];
|
||||
|
||||
for (policy_src, command, matched_prefix) in cases {
|
||||
let expected_reason = format!(
|
||||
"`{}` rejected: policy forbids commands starting with `{}`",
|
||||
render_shlex_command(&command),
|
||||
render_shlex_command(&matched_prefix),
|
||||
);
|
||||
assert_exec_approval_requirement_for_command(
|
||||
ExecApprovalRequirementScenario {
|
||||
policy_src: Some(policy_src),
|
||||
command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
ExecApprovalRequirement::Forbidden {
|
||||
reason: expected_reason,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn unmatched_powershell_wrappers_keep_outer_command_heuristics() {
|
||||
for (command, expected) in [
|
||||
(
|
||||
vec_str(&["powershell.exe", "-Command", "Get-Content Cargo.toml"]),
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec_str(&[
|
||||
"powershell.exe",
|
||||
"-Command",
|
||||
"Get-Content Cargo.toml",
|
||||
]))),
|
||||
},
|
||||
),
|
||||
(
|
||||
vec_str(&[
|
||||
"powershell.exe",
|
||||
"-WindowStyle",
|
||||
"Hidden",
|
||||
"-Command",
|
||||
"Get-Content Cargo.toml",
|
||||
]),
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec_str(&[
|
||||
"powershell.exe",
|
||||
"-WindowStyle",
|
||||
"Hidden",
|
||||
"-Command",
|
||||
"Get-Content Cargo.toml",
|
||||
]))),
|
||||
},
|
||||
),
|
||||
] {
|
||||
assert_exec_approval_requirement_for_command(
|
||||
ExecApprovalRequirementScenario {
|
||||
policy_src: None,
|
||||
command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
expected,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn allow_rule_matches_powershell_wrapped_inner_commands() {
|
||||
assert_exec_approval_requirement_for_command(
|
||||
ExecApprovalRequirementScenario {
|
||||
policy_src: Some(
|
||||
r#"prefix_rule(pattern=["Get-Content"], decision="allow")"#.to_string(),
|
||||
),
|
||||
command: vec_str(&["powershell.exe", "-Command", "Get-Content Cargo.toml"]),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: true,
|
||||
proposed_execpolicy_amendment: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_for_exec_policy_falls_back_for_empty_shell_script() {
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), "".to_string()];
|
||||
|
||||
@@ -81,11 +81,6 @@ fn assert_no_matched_rules_invariant(output_item: &Value) {
|
||||
|
||||
#[tokio::test]
|
||||
async fn execpolicy_blocks_shell_invocation() -> Result<()> {
|
||||
// TODO execpolicy doesn't parse powershell commands yet
|
||||
if cfg!(windows) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
let policy_path = config.codex_home.join("rules").join("policy.rules");
|
||||
fs::create_dir_all(
|
||||
|
||||
@@ -2,4 +2,5 @@ mod powershell_parser;
|
||||
|
||||
pub mod is_dangerous_command;
|
||||
pub mod is_safe_command;
|
||||
pub(crate) use powershell_parser::try_parse_powershell_ast_commands;
|
||||
pub(crate) mod windows_safe_commands;
|
||||
|
||||
@@ -34,6 +34,16 @@ pub(super) fn parse_with_powershell_ast(executable: &str, script: &str) -> Power
|
||||
parse_with_cached_process(&mut parser_processes, executable, script)
|
||||
}
|
||||
|
||||
pub(crate) fn try_parse_powershell_ast_commands(
|
||||
executable: &str,
|
||||
script: &str,
|
||||
) -> Option<Vec<Vec<String>>> {
|
||||
match parse_with_powershell_ast(executable, script) {
|
||||
PowershellParseOutcome::Commands(commands) => Some(commands),
|
||||
PowershellParseOutcome::Unsupported | PowershellParseOutcome::Failed => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum PowershellParseOutcome {
|
||||
Commands(Vec<Vec<String>>),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::command_safety::is_dangerous_command::git_global_option_requires_prompt;
|
||||
use crate::command_safety::powershell_parser::PowershellParseOutcome;
|
||||
use crate::command_safety::powershell_parser::parse_with_powershell_ast;
|
||||
use std::path::Path;
|
||||
use crate::powershell::PowershellCommandSequenceParseMode;
|
||||
use crate::powershell::try_parse_powershell_command_sequence;
|
||||
|
||||
/// 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) {
|
||||
if let Some(commands) = try_parse_powershell_command_sequence(
|
||||
command,
|
||||
PowershellCommandSequenceParseMode::SafeCommand,
|
||||
) {
|
||||
commands
|
||||
.iter()
|
||||
.all(|cmd| is_safe_powershell_command(cmd.as_slice()))
|
||||
@@ -16,130 +18,6 @@ pub fn is_safe_command_windows(command: &[String]) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Vec<Vec<String>>> {
|
||||
let (exe, rest) = command.split_first()?;
|
||||
if is_powershell_executable(exe) {
|
||||
parse_powershell_invocation(exe, rest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a PowerShell invocation into discrete command vectors, rejecting unsafe patterns.
|
||||
fn parse_powershell_invocation(executable: &str, args: &[String]) -> Option<Vec<Vec<String>>> {
|
||||
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 '<script>' or pwsh.exe -Command:<script>
|
||||
fn parse_powershell_script(executable: &str, script: &str) -> Option<Vec<Vec<String>>> {
|
||||
if let PowershellParseOutcome::Commands(commands) =
|
||||
parse_with_powershell_ast(executable, script)
|
||||
{
|
||||
Some(commands)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when the executable name is one of the supported PowerShell binaries.
|
||||
fn is_powershell_executable(exe: &str) -> bool {
|
||||
let executable_name = Path::new(exe)
|
||||
.file_name()
|
||||
.and_then(|osstr| osstr.to_str())
|
||||
.unwrap_or(exe)
|
||||
.to_ascii_lowercase();
|
||||
|
||||
matches!(
|
||||
executable_name.as_str(),
|
||||
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe"
|
||||
)
|
||||
}
|
||||
|
||||
fn join_arguments_as_script(args: &[String]) -> String {
|
||||
let mut words = Vec::with_capacity(args.len());
|
||||
if let Some((first, rest)) = args.split_first() {
|
||||
words.push(first.clone());
|
||||
for arg in rest {
|
||||
words.push(quote_argument(arg));
|
||||
}
|
||||
}
|
||||
words.join(" ")
|
||||
}
|
||||
|
||||
fn quote_argument(arg: &str) -> String {
|
||||
if arg.is_empty() {
|
||||
return "''".to_string();
|
||||
}
|
||||
|
||||
if arg.chars().all(|ch| !ch.is_whitespace()) {
|
||||
return arg.to_string();
|
||||
}
|
||||
|
||||
format!("'{}'", arg.replace('\'', "''"))
|
||||
}
|
||||
|
||||
/// Validates that a parsed PowerShell command stays within our read-only safelist.
|
||||
/// Everything before this is parsing, and rejecting things that make us feel uncomfortable.
|
||||
fn is_safe_powershell_command(words: &[String]) -> bool {
|
||||
@@ -490,6 +368,15 @@ mod tests {
|
||||
"-Command",
|
||||
"''"
|
||||
])));
|
||||
|
||||
// Wrapper flags that exec policy may still parse stay unsafe here.
|
||||
assert!(!is_safe_command_windows(&vec_str(&[
|
||||
"powershell.exe",
|
||||
"-WindowStyle",
|
||||
"Hidden",
|
||||
"-Command",
|
||||
"Get-Content Cargo.toml",
|
||||
])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,10 +2,21 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use crate::command_safety::try_parse_powershell_ast_commands;
|
||||
use crate::shell_detect::ShellType;
|
||||
use crate::shell_detect::detect_shell_type;
|
||||
|
||||
const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
|
||||
const POWERSHELL_NO_ARG_PARSE_FLAGS: &[&str] =
|
||||
&["-nologo", "-noprofile", "-noninteractive", "-mta", "-sta"];
|
||||
const POWERSHELL_VALUE_PARSE_FLAGS: &[&str] =
|
||||
&["-windowstyle", "-executionpolicy", "-workingdirectory"];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PowershellCommandSequenceParseMode {
|
||||
ExecPolicy,
|
||||
SafeCommand,
|
||||
}
|
||||
|
||||
/// Prefixed command for powershell shell calls to force UTF-8 console output.
|
||||
pub const UTF8_OUTPUT_PREFIX: &str = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;\n";
|
||||
@@ -68,6 +79,178 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Recover discrete inner command vectors from an explicit one-layer PowerShell wrapper.
|
||||
///
|
||||
/// This recognizes top-level `powershell` / `powershell.exe` / `pwsh` / `pwsh.exe`
|
||||
/// invocations with an explicit `-Command` / `/Command` / `-c` body and parses that
|
||||
/// script with the PowerShell AST parser. Unsupported or opaque forms such as
|
||||
/// `-EncodedCommand` return `None`.
|
||||
pub fn try_parse_powershell_command_sequence(
|
||||
command: &[String],
|
||||
mode: PowershellCommandSequenceParseMode,
|
||||
) -> Option<Vec<Vec<String>>> {
|
||||
let (executable, args) = command.split_first()?;
|
||||
if is_powershell_executable(executable) {
|
||||
parse_powershell_invocation(executable, args, mode)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_powershell_invocation(
|
||||
executable: &str,
|
||||
args: &[String],
|
||||
mode: PowershellCommandSequenceParseMode,
|
||||
) -> Option<Vec<Vec<String>>> {
|
||||
if args.is_empty() {
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
return parse_powershell_script_to_commands(executable, script);
|
||||
}
|
||||
_ if lower.starts_with("-command:") || lower.starts_with("/command:") => {
|
||||
if idx + 1 != args.len() {
|
||||
return None;
|
||||
}
|
||||
let script = arg.split_once(':')?.1;
|
||||
return parse_powershell_script_to_commands(executable, script);
|
||||
}
|
||||
_ if is_powershell_no_arg_parse_flag(&lower) => {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
_ if is_powershell_value_parse_flag(&lower) => {
|
||||
if mode == PowershellCommandSequenceParseMode::SafeCommand {
|
||||
return None;
|
||||
}
|
||||
args.get(idx + 1)?;
|
||||
idx += 2;
|
||||
continue;
|
||||
}
|
||||
_ if is_powershell_value_parse_flag_with_inline_value(&lower) => {
|
||||
if mode == PowershellCommandSequenceParseMode::SafeCommand {
|
||||
return None;
|
||||
}
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
_ if is_unsupported_powershell_parse_flag(&lower)
|
||||
|| has_unsupported_powershell_parse_flag_inline_value(&lower) =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ if lower.starts_with('-') => {
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
if mode == PowershellCommandSequenceParseMode::ExecPolicy {
|
||||
return None;
|
||||
}
|
||||
|
||||
let script = join_arguments_as_script(&args[idx..]);
|
||||
return parse_powershell_script_to_commands(executable, &script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn parse_powershell_script_to_commands(
|
||||
executable: &str,
|
||||
script: &str,
|
||||
) -> Option<Vec<Vec<String>>> {
|
||||
try_parse_powershell_ast_commands(executable, script)
|
||||
}
|
||||
|
||||
pub(crate) fn is_powershell_executable(exe: &str) -> bool {
|
||||
let executable_name = std::path::Path::new(exe)
|
||||
.file_name()
|
||||
.and_then(|osstr| osstr.to_str())
|
||||
.unwrap_or(exe)
|
||||
.to_ascii_lowercase();
|
||||
|
||||
matches!(
|
||||
executable_name.as_str(),
|
||||
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn join_arguments_as_script(args: &[String]) -> String {
|
||||
let mut words = Vec::with_capacity(args.len());
|
||||
if let Some((first, rest)) = args.split_first() {
|
||||
words.push(first.clone());
|
||||
for arg in rest {
|
||||
words.push(quote_argument(arg));
|
||||
}
|
||||
}
|
||||
words.join(" ")
|
||||
}
|
||||
|
||||
fn quote_argument(arg: &str) -> String {
|
||||
if arg.is_empty() {
|
||||
return "''".to_string();
|
||||
}
|
||||
|
||||
if arg.chars().all(|ch| !ch.is_whitespace()) {
|
||||
return arg.to_string();
|
||||
}
|
||||
|
||||
format!("'{}'", arg.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn is_powershell_no_arg_parse_flag(lower: &str) -> bool {
|
||||
POWERSHELL_NO_ARG_PARSE_FLAGS.contains(&lower)
|
||||
}
|
||||
|
||||
fn is_powershell_value_parse_flag(lower: &str) -> bool {
|
||||
POWERSHELL_VALUE_PARSE_FLAGS.contains(&lower)
|
||||
|| matches!(
|
||||
lower,
|
||||
"/windowstyle" | "/executionpolicy" | "/workingdirectory"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_powershell_value_parse_flag_with_inline_value(lower: &str) -> bool {
|
||||
matches!(
|
||||
split_flag_inline_value(lower),
|
||||
Some((
|
||||
"-windowstyle"
|
||||
| "/windowstyle"
|
||||
| "-executionpolicy"
|
||||
| "/executionpolicy"
|
||||
| "-workingdirectory"
|
||||
| "/workingdirectory",
|
||||
_
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn is_unsupported_powershell_parse_flag(lower: &str) -> bool {
|
||||
matches!(lower, "-encodedcommand" | "-ec" | "-file" | "/file")
|
||||
}
|
||||
|
||||
fn has_unsupported_powershell_parse_flag_inline_value(lower: &str) -> bool {
|
||||
matches!(
|
||||
split_flag_inline_value(lower),
|
||||
Some(("-encodedcommand" | "-ec" | "-file" | "/file", _))
|
||||
)
|
||||
}
|
||||
|
||||
fn split_flag_inline_value(lower: &str) -> Option<(&str, &str)> {
|
||||
lower.split_once(':')
|
||||
}
|
||||
|
||||
/// 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"])
|
||||
|
||||
Reference in New Issue
Block a user