use super::extract_command_basename;
pub fn is_known_safe_command(command: &str) -> bool {
let parts: Vec<&str> = command.split_whitespace().collect();
let Some(first) = parts.first() else {
return false;
};
let cmd_name = extract_command_basename(first);
match cmd_name {
"cat" | "head" | "tail" | "less" | "more" => true,
"ls" | "pwd" | "whoami" | "id" | "uname" | "hostname" | "date" | "uptime" => true,
"grep" | "egrep" | "fgrep" | "wc" | "cut" | "tr" | "sort" | "uniq" | "nl" | "paste"
| "rev" | "seq" | "expr" => true,
"echo" | "printf" | "true" | "false" => true,
"which" | "whereis" | "type" | "file" | "stat" | "realpath" | "basename" | "dirname" => {
true
}
"cd" => true,
"find" => !has_unsafe_find_options(&parts),
"git" => is_safe_git_subcommand(&parts),
"cargo" => matches!(parts.get(1).copied(), Some("check")),
"rg" => !has_unsafe_rg_options(&parts),
"sed" => is_safe_sed_command(&parts),
"base64" => !has_unsafe_base64_options(&parts),
_ => false,
}
}
fn has_unsafe_find_options(parts: &[&str]) -> bool {
const UNSAFE_FIND_OPTIONS: &[&str] = &[
"-exec", "-execdir", "-ok", "-okdir", "-delete", "-fls", "-fprint", "-fprint0", "-fprintf",
];
parts.iter().any(|arg| UNSAFE_FIND_OPTIONS.contains(arg))
}
fn is_safe_git_subcommand(parts: &[&str]) -> bool {
matches!(
parts.get(1).copied(),
Some("status" | "log" | "diff" | "show" | "branch" | "remote" | "tag" | "describe")
)
}
fn has_unsafe_rg_options(parts: &[&str]) -> bool {
parts.iter().any(|arg| {
*arg == "--search-zip"
|| *arg == "-z"
|| *arg == "--pre"
|| arg.starts_with("--pre=")
|| *arg == "--hostname-bin"
|| arg.starts_with("--hostname-bin=")
})
}
fn is_safe_sed_command(parts: &[&str]) -> bool {
if parts.len() < 3 || parts.len() > 4 {
return false;
}
if parts.get(1) != Some(&"-n") {
return false;
}
if let Some(pattern) = parts.get(2) {
is_valid_sed_print_pattern(pattern)
} else {
false
}
}
fn is_valid_sed_print_pattern(pattern: &str) -> bool {
let Some(core) = pattern.strip_suffix('p') else {
return false;
};
let parts: Vec<&str> = core.split(',').collect();
match parts.as_slice() {
[num] => !num.is_empty() && num.chars().all(|c| c.is_ascii_digit()),
[a, b] => {
!a.is_empty()
&& !b.is_empty()
&& a.chars().all(|c| c.is_ascii_digit())
&& b.chars().all(|c| c.is_ascii_digit())
}
_ => false,
}
}
fn has_unsafe_base64_options(parts: &[&str]) -> bool {
parts.iter().any(|arg| {
*arg == "-o" || *arg == "--output" || arg.starts_with("--output=") || arg.starts_with("-o") })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unconditionally_safe_commands() {
assert!(is_known_safe_command("cat file.txt"));
assert!(is_known_safe_command("head -n 10 file.txt"));
assert!(is_known_safe_command("tail -f log.txt"));
assert!(is_known_safe_command("less file.txt"));
assert!(is_known_safe_command("ls -la /tmp"));
assert!(is_known_safe_command("pwd"));
assert!(is_known_safe_command("whoami"));
assert!(is_known_safe_command("id"));
assert!(is_known_safe_command("uname -a"));
assert!(is_known_safe_command("grep pattern file.txt"));
assert!(is_known_safe_command("wc -l file.txt"));
assert!(is_known_safe_command("cut -d: -f1 /etc/passwd"));
assert!(is_known_safe_command("echo hello"));
assert!(is_known_safe_command("printf '%s\\n' hello"));
assert!(is_known_safe_command("which ls"));
assert!(is_known_safe_command("file /bin/ls"));
}
#[test]
fn test_safe_find_commands() {
assert!(is_known_safe_command("find . -name '*.rs'"));
assert!(is_known_safe_command("find /tmp -type f"));
assert!(is_known_safe_command("find . -name '*.txt' -print"));
assert!(is_known_safe_command("/usr/bin/find . -name '*.rs'"));
}
#[test]
fn test_unsafe_find_commands() {
assert!(!is_known_safe_command("find . -exec rm {} \\;"));
assert!(!is_known_safe_command("find . -execdir python {} \\;"));
assert!(!is_known_safe_command("find . -delete"));
assert!(!is_known_safe_command("find . -ok rm {} \\;"));
assert!(!is_known_safe_command("find . -fprint /tmp/out.txt"));
}
#[test]
fn test_safe_git_commands() {
assert!(is_known_safe_command("git status"));
assert!(is_known_safe_command("git log --oneline"));
assert!(is_known_safe_command("git diff HEAD~1"));
assert!(is_known_safe_command("git show HEAD"));
assert!(is_known_safe_command("git branch -a"));
assert!(is_known_safe_command("/usr/bin/git status"));
}
#[test]
fn test_unsafe_git_commands() {
assert!(!is_known_safe_command("git reset --hard"));
assert!(!is_known_safe_command("git rm file.txt"));
assert!(!is_known_safe_command("git push"));
assert!(!is_known_safe_command("git commit -m 'test'"));
assert!(!is_known_safe_command("git checkout -b new-branch"));
}
#[test]
fn test_safe_cargo_commands() {
assert!(is_known_safe_command("cargo check"));
}
#[test]
fn test_unsafe_cargo_commands() {
assert!(!is_known_safe_command("cargo build"));
assert!(!is_known_safe_command("cargo run"));
assert!(!is_known_safe_command("cargo install foo"));
}
#[test]
fn test_safe_rg_commands() {
assert!(is_known_safe_command("rg pattern file.txt"));
assert!(is_known_safe_command("rg -n pattern"));
}
#[test]
fn test_unsafe_rg_commands() {
assert!(!is_known_safe_command("rg --search-zip pattern"));
assert!(!is_known_safe_command("rg -z pattern"));
assert!(!is_known_safe_command("rg --pre=cat pattern"));
assert!(!is_known_safe_command("rg --hostname-bin=hostname pattern"));
}
#[test]
fn test_safe_sed_commands() {
assert!(is_known_safe_command("sed -n 10p file.txt"));
assert!(is_known_safe_command("sed -n 1,5p file.txt"));
}
#[test]
fn test_unsafe_sed_commands() {
assert!(!is_known_safe_command("sed -i 's/foo/bar/' file.txt"));
assert!(!is_known_safe_command("sed 's/foo/bar/' file.txt"));
}
#[test]
fn test_safe_base64_commands() {
assert!(is_known_safe_command("base64 file.txt"));
assert!(is_known_safe_command("base64 -d encoded.txt"));
}
#[test]
fn test_unsafe_base64_commands() {
assert!(!is_known_safe_command("base64 -o out.bin"));
assert!(!is_known_safe_command("base64 --output=out.bin"));
}
#[test]
fn test_unknown_commands() {
assert!(!is_known_safe_command("rm file.txt"));
assert!(!is_known_safe_command("mv a b"));
assert!(!is_known_safe_command("cp a b"));
assert!(!is_known_safe_command("chmod 755 file"));
assert!(!is_known_safe_command("unknown_command"));
}
#[test]
fn test_full_path_commands() {
assert!(is_known_safe_command("/usr/bin/ls -la"));
assert!(is_known_safe_command("/bin/cat file.txt"));
assert!(is_known_safe_command("/usr/local/bin/rg pattern"));
}
#[test]
fn test_empty_command() {
assert!(!is_known_safe_command(""));
assert!(!is_known_safe_command(" "));
}
}