terminal-mcp 0.1.6

Model Context Protocol (MCP) server for long-lived shell execution.
// src/security/detect/bash/rules/obfuscation.rs

use crate::security::detect::{EvaluateResult, Rule, Severity};

/// 反混淆命中聚合规则
///
/// 单一的反混淆手法(比如一次 `\c` 转义)很可能只是普通书写习惯,
/// 并不构成足够的威胁信号,直接告警噪音过大。
/// 因此本规则会汇总当前所有 CommittedBlock 命中的反混淆手法总数
/// (`techniques` + `decode_chain` 累加),只有累计命中数 **超过 3**
/// 时才判定为威胁;否则视为正常噪音,返回 `Miss`。
///
/// 若命中中包含任意非空的 `decode_chain`(说明该块是从
/// eval / base64|bash 等执行汇聚点递归解码展开出来的),
/// 风险显著更高,动态提升为 Medium;否则维持 Low。
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;

        // 汇总所有 block 的命中详情,同时统计总命中数
        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))
    }
}