use super::parser::{parse_for_security, ParseResult};
pub fn destructive_rm_verdict(command: &str) -> Option<(&'static str, String)> {
let parsed = parse_for_security(command);
let commands = match parsed {
ParseResult::Simple { commands } => commands,
_ => return None,
};
for cmd in &commands {
let mut idx = 0usize;
if cmd.argv.first().map(|a| a == "sudo").unwrap_or(false) {
idx = 1;
}
if cmd.argv.get(idx).map(|a| a.as_str()) != Some("rm") {
continue;
}
let args = &cmd.argv[idx + 1..];
let mut recursive = false;
let mut targets: Vec<&str> = Vec::new();
let mut end_of_options = false;
for a in args {
if !end_of_options && a == "--" {
end_of_options = true;
continue;
}
if !end_of_options && a.starts_with('-') && a.len() > 1 {
if a == "--recursive" {
recursive = true;
} else if a.starts_with("--") {
} else {
let body = &a[1..];
if !body.contains('=') && body.chars().any(|c| c == 'r' || c == 'R') {
recursive = true;
}
}
} else {
targets.push(a.as_str());
}
}
let matched = cmd.argv.join(" ");
if targets.iter().any(|t| is_root_path(t)) {
return Some(("rm_root", matched));
}
if recursive && !targets.is_empty() {
return Some(("rm_recursive", matched));
}
}
None
}
fn is_root_path(target: &str) -> bool {
let t = target.trim();
if t == "/" {
return true;
}
let bytes = t.as_bytes();
if bytes.len() == 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/')
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn verdict_name(cmd: &str) -> Option<&'static str> {
destructive_rm_verdict(cmd).map(|(n, _)| n)
}
#[test]
fn a06_rm_recursive_long_options_root_is_deny() {
assert_eq!(verdict_name("rm --recursive --force /"), Some("rm_root"));
assert_eq!(verdict_name("rm -rf --no-preserve-root /"), Some("rm_root"));
}
#[test]
fn a06_rm_recursive_long_options_dir_is_confirm() {
assert_eq!(verdict_name("rm --recursive dir"), Some("rm_recursive"));
assert_eq!(
verdict_name("rm --recursive --force build/"),
Some("rm_recursive")
);
assert_eq!(verdict_name("rm -fr build/"), Some("rm_recursive"));
assert_eq!(verdict_name("rm -Rf build/"), Some("rm_recursive"));
}
#[test]
fn a06_rm_root_without_flags() {
assert_eq!(verdict_name("rm /"), Some("rm_root"));
assert_eq!(verdict_name("rm -f /"), Some("rm_root"));
assert_eq!(verdict_name("rm / C:\\Users"), Some("rm_root"));
}
#[test]
fn a06_windows_drive_root() {
assert_eq!(verdict_name("rm -rf 'C:\\'"), Some("rm_root"));
assert_eq!(verdict_name("rm -rf c:/"), Some("rm_root"));
assert_eq!(verdict_name("rm -rf C:/tmp"), Some("rm_recursive"));
}
#[test]
fn a06_sudo_prefix_stripped() {
assert_eq!(verdict_name("sudo rm -rf /"), Some("rm_root"));
assert_eq!(
verdict_name("sudo rm --recursive dir"),
Some("rm_recursive")
);
}
#[test]
fn a06_non_recursive_rm_not_flagged() {
assert_eq!(verdict_name("rm file.txt"), None);
assert_eq!(verdict_name("rm -f file.txt"), None);
assert_eq!(verdict_name("rm -- file.txt"), None);
}
#[test]
fn a06_recursive_without_target_not_flagged() {
assert_eq!(verdict_name("rm -r"), None);
assert_eq!(verdict_name("rm --recursive"), None);
}
#[test]
fn a06_other_commands_not_flagged() {
assert_eq!(verdict_name("ls -r"), None);
assert_eq!(verdict_name("grep -r pattern dir"), None);
assert_eq!(verdict_name("rm -i file"), None);
assert_eq!(verdict_name("rm --dry-run file"), None);
}
#[test]
fn a06_unparseable_returns_none_for_regex_fallback() {
assert_eq!(destructive_rm_verdict("rm -rf $(compute_target)"), None);
}
}