#![allow(dead_code)]
mod shell_safety;
mod tool_safety;
use shell_safety::is_safe_shell_command;
use tool_safety::{is_safe_find, is_safe_git, is_safe_ripgrep, is_safe_sed};
#[must_use]
pub fn is_safe_command(command: &[String]) -> bool {
let Some(cmd) = command.first() else {
return false;
};
let binary = std::path::Path::new(cmd)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(cmd);
let binary = if binary == "zsh" { "bash" } else { binary };
if is_safe_binary(binary, command) {
return true;
}
if is_safe_shell_command(binary, command) {
return true;
}
false
}
fn is_safe_binary(binary: &str, command: &[String]) -> bool {
match binary {
"cat" | "cd" | "cut" | "echo" | "expr" | "false" | "head" | "id" | "ls" | "nl"
| "paste" | "pwd" | "rev" | "seq" | "stat" | "tail" | "tr" | "true" | "uname" | "uniq"
| "wc" | "which" | "whoami" | "hostname" | "date" | "env" | "printenv" | "file"
| "type" | "basename" | "dirname" | "realpath" | "readlink" => true,
"grep" | "egrep" | "fgrep" => true,
"find" => is_safe_find(command),
"rg" => is_safe_ripgrep(command),
"git" => is_safe_git(command),
"cargo" => command.get(1).is_some_and(|sub| sub == "check"),
"sed" => is_safe_sed(command),
"sort" => !command.iter().any(|a| a.starts_with("-o")),
"diff" => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cmd(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
#[test]
fn basic_safe_commands() {
assert!(is_safe_command(&cmd(&["ls"])));
assert!(is_safe_command(&cmd(&["ls", "-la"])));
assert!(is_safe_command(&cmd(&["cat", "file.txt"])));
assert!(is_safe_command(&cmd(&["pwd"])));
assert!(is_safe_command(&cmd(&["whoami"])));
assert!(is_safe_command(&cmd(&["echo", "hello"])));
}
#[test]
fn git_safe_subcommands() {
assert!(is_safe_command(&cmd(&["git", "status"])));
assert!(is_safe_command(&cmd(&["git", "log"])));
assert!(is_safe_command(&cmd(&["git", "diff"])));
assert!(!is_safe_command(&cmd(&["git", "push"])));
assert!(!is_safe_command(&cmd(&["git", "commit"])));
}
#[test]
fn find_safety() {
assert!(is_safe_command(&cmd(&["find", ".", "-name", "*.rs"])));
assert!(!is_safe_command(&cmd(&["find", ".", "-delete"])));
assert!(!is_safe_command(&cmd(&[
"find", ".", "-exec", "rm", "{}", ";"
])));
}
#[test]
fn ripgrep_safety() {
assert!(is_safe_command(&cmd(&["rg", "pattern", "-n"])));
assert!(!is_safe_command(&cmd(&["rg", "--pre", "cmd", "pattern"])));
assert!(!is_safe_command(&cmd(&["rg", "-z", "pattern"])));
}
#[test]
fn shell_script_safety() {
assert!(is_safe_command(&cmd(&["bash", "-c", "ls && pwd"])));
assert!(is_safe_command(&cmd(&["bash", "-lc", "git status"])));
assert!(!is_safe_command(&cmd(&["bash", "-c", "rm -rf /"])));
assert!(!is_safe_command(&cmd(&["bash", "-c", "ls > out.txt"])));
}
#[test]
fn sed_safety() {
assert!(is_safe_command(&cmd(&["sed", "-n", "1,5p", "file.txt"])));
assert!(is_safe_command(&cmd(&["sed", "-n", "10p"])));
assert!(!is_safe_command(&cmd(&["sed", "-i", "s/a/b/g"])));
}
#[test]
fn unsafe_commands() {
assert!(!is_safe_command(&cmd(&["rm", "file"])));
assert!(!is_safe_command(&cmd(&["mv", "a", "b"])));
assert!(!is_safe_command(&cmd(&["chmod", "+x", "file"])));
assert!(!is_safe_command(&cmd(&["curl", "url"])));
}
}