Codex Handbook
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));        }    }}