use crate::security::detect::{EvaluateResult, Rule, Severity};
pub struct RuleBashObfuscationEvidence;
const OBFUSCATION_HIT_THRESHOLD: usize = 4;
impl RuleBashObfuscationEvidence {
fn get_meta() -> &'static crate::security::detect::RuleMetadata {
static META: std::sync::LazyLock<crate::security::detect::RuleMetadata> =
std::sync::LazyLock::new(|| crate::security::detect::RuleMetadata {
name: "bash_obfuscation_evidence".to_string(),
description:
"Detects string obfuscation / encoding pipelines / eval wrapping used to hide the real command; \
only triggers when the accumulated hit count exceeds a noise threshold"
.to_string(),
default_severity: Severity::Low,
});
&META
}
}
#[async_trait::async_trait]
impl Rule for RuleBashObfuscationEvidence {
fn meta(&self) -> &crate::security::detect::RuleMetadata {
Self::get_meta()
}
async fn evaluate(
&self,
_data: &str,
ctx: &crate::security::detect::ShellContext,
) -> anyhow::Result<EvaluateResult> {
use crate::security::detect::bash::ast::CurrentAst;
let current = ctx
.extensions
.get::<CurrentAst>()
.ok_or_else(|| anyhow::anyhow!("CurrentAst missing"))?;
let blocks = current.blocks.read().await;
let mut total_hits = 0usize;
let mut has_decode_chain = false;
let mut per_block_evidence: Vec<String> = Vec::new();
for block in blocks.iter() {
let block_hits = block.deobf.techniques.len() + block.deobf.decode_chain.len();
if block_hits == 0 {
continue;
}
total_hits += block_hits;
if !block.deobf.decode_chain.is_empty() {
has_decode_chain = true;
}
per_block_evidence.push(format!(
"techniques={:?} decode_chain={:?} source={:?}",
block.deobf.techniques,
block.deobf.decode_chain,
block.source,
));
}
if total_hits <= OBFUSCATION_HIT_THRESHOLD {
return Ok(EvaluateResult::Miss);
}
let evidence = format!(
"total_hits={} threshold={} blocks=[{}]",
total_hits,
OBFUSCATION_HIT_THRESHOLD,
per_block_evidence.join("; "),
);
let severity = if has_decode_chain {
Severity::Medium
} else {
Severity::Low
};
Ok(EvaluateResult::hit_with_severity(evidence, severity))
}
}