shell-command/src/command_safety/is_dangerous_command.rs
187 lines
use crate::bash::parse_shell_lc_plain_commands;use std::path::Path;#[cfg(windows)]#[path = "windows_dangerous_commands.rs"]mod windows_dangerous_commands;pub fn command_might_be_dangerous(command: &[String]) -> bool { #[cfg(windows)] { if windows_dangerous_commands::is_dangerous_command_windows(command) { return true; } } if is_dangerous_to_call_with_exec(command) { return true; } // Support `bash -lc "<script>"` where the any part of the script might contain a dangerous command. if let Some(all_commands) = parse_shell_lc_plain_commands(command) && all_commands .iter() .any(|cmd| is_dangerous_to_call_with_exec(cmd)) { return true; } false}/// Returns whether already-tokenized PowerShell words should be treated as/// dangerous by the Windows unmatched-command heuristics.pub fn is_dangerous_powershell_words(command: &[String]) -> bool { #[cfg(windows)] { windows_dangerous_commands::is_dangerous_powershell_words(command) } #[cfg(not(windows))] { let _ = command; false }}fn is_git_global_option_with_value(arg: &str) -> bool { matches!( arg, "-C" | "-c" | "--config-env" | "--exec-path" | "--git-dir" | "--namespace" | "--super-prefix" | "--work-tree" )}fn is_git_global_option_with_inline_value(arg: &str) -> bool { matches!( arg, s if s.starts_with("--config-env=") || s.starts_with("--exec-path=") || s.starts_with("--git-dir=") || s.starts_with("--namespace=") || s.starts_with("--super-prefix=") || s.starts_with("--work-tree=") ) || ((arg.starts_with("-C") || arg.starts_with("-c")) && arg.len() > 2)}pub(crate) fn executable_name_lookup_key(raw: &str) -> Option<String> { #[cfg(windows)] { Path::new(raw) .file_name() .and_then(|name| name.to_str()) .map(|name| { let name = name.to_ascii_lowercase(); for suffix in [".exe", ".cmd", ".bat", ".com"] { if let Some(stripped) = name.strip_suffix(suffix) { return stripped.to_string(); } } name }) } #[cfg(not(windows))] { Path::new(raw) .file_name() .and_then(|name| name.to_str()) .map(std::borrow::ToOwned::to_owned) }}/// Find the first matching git subcommand, skipping known global options that/// may appear before it (e.g., `-C`, `-c`, `--git-dir`).////// Shared with `is_safe_command` to avoid git-global-option bypasses.pub(crate) fn find_git_subcommand<'a>( command: &'a [String], subcommands: &[&str],) -> Option<(usize, &'a str)> { let cmd0 = command.first().map(String::as_str)?; if executable_name_lookup_key(cmd0).as_deref() != Some("git") { return None; } let mut skip_next = false; for (idx, arg) in command.iter().enumerate().skip(1) { if skip_next { skip_next = false; continue; } let arg = arg.as_str(); if is_git_global_option_with_inline_value(arg) { continue; } if is_git_global_option_with_value(arg) { skip_next = true; continue; } if arg == "--" || arg.starts_with('-') { continue; } if subcommands.contains(&arg) { return Some((idx, arg)); } // In git, the first non-option token is the subcommand. If it isn't // one of the subcommands we're looking for, we must stop scanning to // avoid misclassifying later positional args (e.g., branch names). return None; } None}fn is_dangerous_to_call_with_exec(command: &[String]) -> bool { let cmd0 = command.first().map(String::as_str); match cmd0 { Some("rm") => matches!(command.get(1).map(String::as_str), Some("-f" | "-rf")), // for sudo <cmd> simply do the check for <cmd> Some("sudo") => is_dangerous_to_call_with_exec(&command[1..]), // ── anything else ───────────────────────────────────────────────── _ => false, }}#[cfg(test)]mod tests { use super::*; fn vec_str(items: &[&str]) -> Vec<String> { items.iter().map(std::string::ToString::to_string).collect() } #[test] fn rm_rf_is_dangerous() { assert!(command_might_be_dangerous(&vec_str(&["rm", "-rf", "/"]))); } #[test] fn rm_f_is_dangerous() { assert!(command_might_be_dangerous(&vec_str(&["rm", "-f", "/"]))); } #[test] fn direct_powershell_words_reuse_windows_dangerous_detection() { let command = vec_str(&["Remove-Item", "test", "-Force"]); if cfg!(windows) { assert!(is_dangerous_powershell_words(&command)); } else { assert!(!is_dangerous_powershell_words(&command)); } }}