use super::{DangerousCommandClass, ExecMediationPolicy, ExecRiskTier, SecretBrokerPolicy};
impl DangerousCommandClass {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::RecursiveDelete => "recursive_delete",
Self::DeviceWrite => "device_write",
Self::ForkBomb => "fork_bomb",
Self::PipeToShell => "pipe_to_shell",
Self::SystemShutdown => "system_shutdown",
Self::PermissionEscalation => "permission_escalation",
Self::ProcessTermination => "process_termination",
Self::CredentialFileModification => "credential_file_modification",
Self::DiskWipe => "disk_wipe",
Self::ReverseShell => "reverse_shell",
}
}
#[must_use]
pub const fn risk_tier(self) -> ExecRiskTier {
match self {
Self::RecursiveDelete
| Self::DeviceWrite
| Self::ForkBomb
| Self::DiskWipe
| Self::ReverseShell => ExecRiskTier::Critical,
Self::PipeToShell
| Self::SystemShutdown
| Self::PermissionEscalation
| Self::ProcessTermination
| Self::CredentialFileModification => ExecRiskTier::High,
}
}
}
impl Default for ExecMediationPolicy {
fn default() -> Self {
Self {
enabled: true,
deny_threshold: ExecRiskTier::Critical,
deny_patterns: Vec::new(),
allow_patterns: Vec::new(),
audit_all_classified: true,
}
}
}
impl ExecMediationPolicy {
#[must_use]
pub const fn strict() -> Self {
Self {
enabled: true,
deny_threshold: ExecRiskTier::High,
deny_patterns: Vec::new(),
allow_patterns: Vec::new(),
audit_all_classified: true,
}
}
#[must_use]
pub const fn permissive() -> Self {
Self {
enabled: true,
deny_threshold: ExecRiskTier::Critical,
deny_patterns: Vec::new(),
allow_patterns: Vec::new(),
audit_all_classified: false,
}
}
#[must_use]
pub const fn disabled() -> Self {
Self {
enabled: false,
deny_threshold: ExecRiskTier::Critical,
deny_patterns: Vec::new(),
allow_patterns: Vec::new(),
audit_all_classified: false,
}
}
}
pub(super) fn normalize_command_for_classification(command: &str) -> String {
let mut normalized = String::with_capacity(command.len());
let mut previous_was_space = false;
let mut remaining = command;
while !remaining.is_empty() {
if let Some(rest) = remaining.strip_prefix("${ifs}") {
if !previous_was_space {
normalized.push(' ');
previous_was_space = true;
}
remaining = rest;
continue;
}
if let Some(rest) = remaining.strip_prefix("$ifs") {
if !previous_was_space {
normalized.push(' ');
previous_was_space = true;
}
remaining = rest;
continue;
}
let mut chars = remaining.chars();
let Some(mut ch) = chars.next() else {
break;
};
if ch == '\'' || ch == '"' {
remaining = chars.as_str();
continue;
}
if ch == '\\' {
let mut peek_chars = chars.clone();
if let Some(next) = peek_chars.next() {
if next == '\n' || next == '\r' {
remaining = peek_chars.as_str();
continue;
}
chars.next();
if next.is_ascii_whitespace() {
if !previous_was_space {
normalized.push(' ');
previous_was_space = true;
}
remaining = chars.as_str();
continue;
}
if next == '\'' || next == '"' {
remaining = chars.as_str();
continue;
}
ch = next;
}
}
if ch.is_ascii_whitespace() {
if !previous_was_space {
normalized.push(' ');
previous_was_space = true;
}
} else {
normalized.push(ch);
previous_was_space = false;
}
remaining = chars.as_str();
}
normalized
}
pub(super) fn classify_recursive_delete(lower: &str) -> bool {
if !lower.contains("rm") {
return false;
}
let has_rf = lower.contains("-rf")
|| lower.contains("-fr")
|| lower.contains("--recursive")
|| (lower.contains("-r") && lower.contains("-f"));
if !has_rf {
return false;
}
let dangerous_targets = [" /", " /*", " /.", " ~/", " ~/*", " --no-preserve-root"];
dangerous_targets.iter().any(|t| lower.contains(t))
}
pub(super) fn classify_device_write(lower: &str) -> bool {
let dd_to_dev = lower.contains("dd ") && lower.contains("of=/dev/");
let mkfs = lower.starts_with("mkfs") || lower.contains(" mkfs") || lower.contains(";mkfs");
let fdisk = lower.starts_with("fdisk") || lower.contains(" fdisk") || lower.contains(";fdisk");
dd_to_dev || mkfs || fdisk
}
pub(super) fn classify_fork_bomb(lower: &str) -> bool {
lower.contains(":(){ :|:&")
|| lower.contains(":(){ :|: &")
|| (lower.contains("while true") && lower.contains("& done"))
|| (lower.contains("fork") && lower.contains("while") && lower.contains('&'))
}
pub(super) fn classify_disk_wipe(lower: &str) -> bool {
let shred = lower.starts_with("shred") || lower.contains(" shred ") || lower.contains(";shred");
let wipefs =
lower.starts_with("wipefs") || lower.contains(" wipefs") || lower.contains(";wipefs");
let dd_zero = lower.contains("dd ") && lower.contains("if=/dev/zero");
let dd_urandom = lower.contains("dd ") && lower.contains("if=/dev/urandom");
shred || wipefs || dd_zero || dd_urandom
}
pub(super) fn classify_reverse_shell(lower: &str) -> bool {
let bash_rev = lower.contains("/dev/tcp/") && lower.contains("bash");
let nc_rev = (lower.contains("nc ") || lower.contains("ncat ") || lower.contains("netcat "))
&& lower.contains("-e ");
let python_rev = lower.contains("socket") && lower.contains("connect") && lower.contains("sh");
bash_rev || nc_rev || python_rev
}
pub(super) fn classify_pipe_to_shell(lower: &str) -> bool {
const PIPE_SHELL_PATTERNS: &[&str] = &[
"| sh",
"| bash",
"|sh",
"|bash",
"| /bin/sh",
"| /bin/bash",
"|/bin/sh",
"|/bin/bash",
"| /usr/bin/sh",
"| /usr/bin/bash",
"|/usr/bin/sh",
"|/usr/bin/bash",
"| /usr/local/bin/sh",
"| /usr/local/bin/bash",
"|/usr/local/bin/sh",
"|/usr/local/bin/bash",
];
let has_download = lower.contains("curl ") || lower.contains("wget ");
let has_pipe_to_shell = PIPE_SHELL_PATTERNS.iter().any(|p| lower.contains(p));
let download_exec_patterns = [
"eval \"$(curl ",
"eval \"$(wget ",
"eval '$(curl ",
"eval '$(wget ",
"eval $(curl ",
"eval $(wget ",
"source <(curl ",
"source <(wget ",
"bash -c \"$(curl ",
"bash -c \"$(wget ",
"bash -c '$(curl ",
"bash -c '$(wget ",
"sh -c \"$(curl ",
"sh -c \"$(wget ",
"sh -c '$(curl ",
"sh -c '$(wget ",
];
(has_download && has_pipe_to_shell)
|| download_exec_patterns
.iter()
.any(|pattern| lower.contains(pattern))
}
pub(super) fn classify_system_shutdown(lower: &str) -> bool {
lower.starts_with("shutdown")
|| lower.contains(" shutdown")
|| lower.contains(";shutdown")
|| lower.starts_with("reboot")
|| lower.contains(" reboot")
|| lower.contains(";reboot")
|| lower.starts_with("halt")
|| lower.contains(" halt")
|| lower.contains(";halt")
|| lower.starts_with("poweroff")
|| lower.contains(" poweroff")
|| lower.contains(";poweroff")
|| lower.starts_with("init 0")
|| lower.contains(" init 0")
|| lower.starts_with("init 6")
|| lower.contains(" init 6")
}
pub(super) fn classify_permission_escalation(lower: &str) -> bool {
let chmod_broad = lower.contains("chmod")
&& (lower.contains("777") || lower.contains("a+rwx") || lower.contains("o+w"));
let chmod_suid = lower.contains("chmod") && (lower.contains("+s") || lower.contains("4755"));
chmod_broad || chmod_suid
}
pub(super) fn classify_process_termination(lower: &str) -> bool {
let kill_pid1 = lower.contains("kill") && (lower.contains(" 1 ") || lower.ends_with(" 1"));
let kill_9 = lower.contains("kill -9") || lower.contains("kill -kill");
let pkill_critical = lower.contains("pkill")
&& (lower.contains("init") || lower.contains("systemd") || lower.contains("sshd"));
let killall = lower.starts_with("killall") || lower.contains(" killall");
(kill_pid1 && kill_9) || pkill_critical || killall
}
pub(super) fn classify_credential_file_modification(lower: &str) -> bool {
let cred_files = [
"/etc/passwd",
"/etc/shadow",
"/etc/sudoers",
"/etc/ssh/sshd_config",
];
let write_cmds = ["tee ", "cat >", "echo >", "sed -i", "cp ", "mv "];
cred_files
.iter()
.any(|f| lower.contains(f) && write_cmds.iter().any(|w| lower.contains(w)))
}
impl ExecRiskTier {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
}
impl Default for SecretBrokerPolicy {
fn default() -> Self {
Self {
enabled: true,
secret_suffixes: vec![
"_KEY".to_string(),
"_SECRET".to_string(),
"_TOKEN".to_string(),
"_PASSWORD".to_string(),
"_PASSWD".to_string(),
"_CREDENTIAL".to_string(),
"_CREDENTIALS".to_string(),
"_AUTH".to_string(),
"_API_KEY".to_string(),
"_PRIVATE_KEY".to_string(),
],
secret_prefixes: vec![
"SECRET_".to_string(),
"AUTH_".to_string(),
"CREDENTIAL_".to_string(),
],
secret_exact: vec![
"ANTHROPIC_API_KEY".to_string(),
"OPENAI_API_KEY".to_string(),
"AWS_SECRET_ACCESS_KEY".to_string(),
"AWS_SESSION_TOKEN".to_string(),
"GITHUB_TOKEN".to_string(),
"GOOGLE_API_KEY".to_string(),
"AZURE_CLIENT_SECRET".to_string(),
"DATABASE_URL".to_string(),
"REDIS_URL".to_string(),
"PRIVATE_KEY".to_string(),
"NPM_TOKEN".to_string(),
"DOCKER_PASSWORD".to_string(),
"SLACK_TOKEN".to_string(),
"STRIPE_SECRET_KEY".to_string(),
"TWILIO_AUTH_TOKEN".to_string(),
"SENDGRID_API_KEY".to_string(),
],
disclosure_allowlist: Vec::new(),
redaction_placeholder: "[REDACTED]".to_string(),
}
}
}
impl SecretBrokerPolicy {
#[must_use]
pub fn is_secret(&self, name: &str) -> bool {
if !self.enabled {
return false;
}
let upper = name.to_ascii_uppercase();
if self
.disclosure_allowlist
.iter()
.any(|a| a.eq_ignore_ascii_case(name))
{
return false;
}
if self
.secret_exact
.iter()
.any(|e| e.eq_ignore_ascii_case(name))
{
return true;
}
if self
.secret_suffixes
.iter()
.any(|s| upper.ends_with(&s.to_ascii_uppercase()))
{
return true;
}
self.secret_prefixes
.iter()
.any(|p| upper.starts_with(&p.to_ascii_uppercase()))
}
#[must_use]
pub fn maybe_redact<'a>(&'a self, name: &str, value: &'a str) -> &'a str {
if self.is_secret(name) {
&self.redaction_placeholder
} else {
value
}
}
}