terminal-mcp 0.1.5

Model Context Protocol (MCP) server for long-lived shell execution.
// src/security/detect/powershell/utils.rs

use std::sync::LazyLock;
use tree_sitter::{Node, Query, QueryCursor, StreamingIterator};

use crate::security::detect::powershell::ast::{CurrentAst, get_command_name, language};
use crate::security::detect::utils::{command_basename, node_extract_text};
use crate::security::detect::{Severity, ShellContext};

/// 简单清理 PowerShell 字符串(去掉首尾的引号)。
pub fn clean_ps_string(s: &str) -> String {
    let s = s.trim();
    if s.len() >= 2
        && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
    {
        return s[1..s.len() - 1].to_string();
    }
    s.to_string()
}

/// 从任意文本中提取第一个 http(s) URL(供下载/远程执行判定)。
pub fn extract_url_from_text(text: &str) -> Option<String> {
    static URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
        regex::Regex::new("https?://[^\\s'\\\"`]+").expect("valid url regex")
    });
    URL_RE.find(text).map(|m| m.as_str().to_string())
}

/// PowerShell 下载器判定(含 cmdlet 别名与 .NET 下载方法名)。
pub fn is_ps_downloader(name: &str) -> bool {
    let lower = name.to_lowercase();
    let base = command_basename(&lower);
    matches!(
        base.as_str(),
        "iwr"
            | "invoke-webrequest"
            | "curl"
            | "wget"
            | "curl.exe"
            | "downloadstring"
            | "downloaddata"
            | "downloadfile"
    )
}

/// 覆盖 command 与 redirection 与 invokation_expression 的公共查询。
static COMMON_QUERY: LazyLock<Query> = LazyLock::new(|| {
    Query::new(
        &language(),
        "[(command) @cmd (redirection) @redirect (invokation_expression) @inv]",
    )
    .expect("invalid query")
});

/// 遍历当前所有反混淆块,对每个捕获节点运行 `analyze`,
/// 返回命中中最高严重度的一条。
pub async fn best_hit(
    ctx: &ShellContext,
    analyze: impl Fn(&Node, &[u8]) -> Option<(Severity, String)>,
) -> anyhow::Result<Option<(Severity, String)>> {
    let current = ctx
        .extensions
        .get::<CurrentAst>()
        .ok_or_else(|| anyhow::anyhow!("CurrentAst missing"))?;
    let blocks = current.blocks.read().await;

    let query = &*COMMON_QUERY;
    let mut best: Option<(Severity, String)> = None;

    for block in blocks.iter() {
        let source_bytes = block.source.as_bytes();
        let mut cursor = QueryCursor::new();
        let mut matches = cursor.matches(query, block.tree.root_node(), source_bytes);
        while let Some(m) = StreamingIterator::next(&mut matches) {
            for capture in m.captures {
                if let Some(hit) = analyze(&capture.node, source_bytes)
                    && best.as_ref().is_none_or(|(b, _)| hit.0 > *b)
                {
                    best = Some(hit);
                }
            }
        }
    }

    Ok(best)
}

/// 遍历当前所有反混淆块的**文本**,运行 `analyze`,返回最高严重度命中。
/// 用于难以用 AST 节点精确刻画、需整块文本启发式判定的规则。
pub async fn best_text(
    ctx: &ShellContext,
    analyze: impl Fn(&str) -> Option<(Severity, String)>,
) -> anyhow::Result<Option<(Severity, String)>> {
    let current = ctx
        .extensions
        .get::<CurrentAst>()
        .ok_or_else(|| anyhow::anyhow!("CurrentAst missing"))?;
    let blocks = current.blocks.read().await;

    let mut best: Option<(Severity, String)> = None;
    for block in blocks.iter() {
        if let Some(hit) = analyze(&block.source)
            && best.as_ref().is_none_or(|(b, _)| hit.0 > *b)
        {
            best = Some(hit);
        }
    }
    Ok(best)
}

/// 收集 command 节点 field `command_elements` 下的参数文本
/// (不含命令名 / command_parameter 标志 / 重定向 / 参数分隔符)。
pub fn collect_args<'a>(node: &Node, source: &'a [u8]) -> Vec<&'a str> {
    collect_elements(node, source)
        .into_iter()
        .filter(|(kind, _)| !matches!(kind.as_str(), "command_parameter" | "redirection"))
        .map(|(_, text)| text)
        .collect()
}

/// 收集 command 节点 field `command_elements` 下的 (kind, text) 列表。
pub fn collect_elements<'a>(node: &Node, source: &'a [u8]) -> Vec<(String, &'a str)> {
    let mut out = Vec::new();
    let mut cursor = node.walk();

    for child in node.children(&mut cursor) {
        if child.kind() == "command_elements" {
            let mut ec = child.walk();
            for el in child.children(&mut ec) {
                if el.kind() == "command_argument_sep" {
                    continue;
                }
                if let Some(t) = node_extract_text(&el, source) {
                    out.push((el.kind().to_string(), t));
                }
            }
        }
    }
    out
}

/// 收集 command 节点的参数标志名(command_parameter 文本,如 `-Recurse`)。
pub fn collect_parameters<'a>(node: &Node, source: &'a [u8]) -> Vec<&'a str> {
    collect_elements(node, source)
        .into_iter()
        .filter(|(kind, _)| kind == "command_parameter")
        .map(|(_, text)| text)
        .collect()
}

/// 解析 redirection 节点:返回 (操作符文本, 目标路径)。
pub fn redirect_target<'a>(node: &Node, source: &'a [u8]) -> Option<(&'a str, &'a str)> {
    if node.kind() != "redirection" {
        return None;
    }
    let mut operator: Option<&str> = None;
    let mut dest: Option<&'a str> = None;

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        match child.kind() {
            "file_redirection_operator" => {
                operator = node_extract_text(&child, source);
            }
            "redirected_file_name" => {
                dest = node_extract_text(&child, source);
            }
            _ => {}
        }
    }

    match (operator, dest) {
        (Some(op), Some(dest)) => Some((op, dest)),
        _ => None,
    }
}

/// Windows 路径归一化:去首尾空白/引号、小写盘符、去尾部多余 `\\` 与 `/`。
pub fn ps_normalize_path(t: &str) -> String {
    let mut s = t.trim().trim_matches(|c| c == '\'' || c == '"').to_string();

    // C:\ -> c:\,便于统一比较
    if s.len() >= 2 && s.as_bytes()[1] == b':' {
        let drive = s.as_bytes()[0].to_ascii_lowercase() as char;
        s.replace_range(0..1, &drive.to_string());
    }

    while s.len() > 1 && (s.ends_with('\\') || s.ends_with('/')) {
        s.pop();
    }
    s
}

/// Windows 路径是否命中任一标记(大小写不敏感)。
pub fn ps_path_has_marker(p: &str, markers: &[&str]) -> bool {
    let s = ps_normalize_path(p).to_lowercase();
    markers.iter().any(|m| s.contains(&m.to_lowercase()))
}

/// 检查参数列表中是否包含指定参数名(大小写不敏感,如 `-Recurse`/`-Force`)。
pub fn has_parameter(args: &[&str], param: &str) -> bool {
    let param_lower = param.to_lowercase();
    args.iter().any(|a| {
        let t = a.trim_start_matches('-').trim_start_matches('/');
        t.to_lowercase() == param_lower
    })
}

/// 检查参数列表中是否包含任一指定参数名。
pub fn has_any_parameter(args: &[&str], params: &[&str]) -> bool {
    params.iter().any(|p| has_parameter(args, p))
}

/// 是否为 PowerShell 执行汇聚点(iex / Invoke-Expression / pwsh / powershell / cmd)。
pub fn is_ps_shell_sink(name: &str) -> bool {
    let lower = name.to_lowercase();
    matches!(
        lower.as_str(),
        "iex"
            | "invoke-expression"
            | "powershell"
            | "pwsh"
            | "powershell.exe"
            | "pwsh.exe"
            | "cmd"
            | "cmd.exe"
    )
}

/// 从任意文本中定位执行汇聚点(sink)与下载器并存时的下载 URL。
/// 返回 (sink 命令名, URL)。
pub fn detect_remote_execution_text(text: &str) -> Option<(String, String)> {
    let lower = text.to_lowercase();
    let sink = if lower.contains("invoke-expression") || lower.contains("iex") {
        "iex"
    } else if lower.contains("pwsh") || lower.contains("powershell") {
        "powershell"
    } else if lower.contains("cmd") {
        "cmd"
    } else {
        return None;
    };
    let url = extract_url_from_text(text)?;
    Some((sink.to_string(), url))
}

/// 在节点子树中 DFS 查找下载器命令并提取 URL。
pub fn extract_downloader_url(node: &Node, source: &[u8]) -> Option<String> {
    let mut stack = vec![*node];
    while let Some(n) = stack.pop() {
        if n.kind() == "command"
            && let Some(cmd_name) = get_command_name(&n, source)
            && is_ps_downloader(cmd_name)
        {
            for arg in collect_args(&n, source) {
                let cleaned = clean_ps_string(arg);
                if cleaned.starts_with("http://") || cleaned.starts_with("https://") {
                    return Some(cleaned);
                }
            }
        }

        let mut cursor = n.walk();
        for child in n.children(&mut cursor) {
            stack.push(child);
        }
    }
    None
}
//
// /// 检查 command 节点是否以 `cmd /c` 形式包装了 Windows 原生命令字符串,
// /// 若是则返回解包后的参数字符串(如 `rmdir /s /q C:\`)。
// pub fn unwrap_cmd_wrapper<'a>(node: &Node, source: &'a [u8]) -> Option<String> {
//     let name = get_command_name(node, source)?;
//     if !matches!(name.to_lowercase().as_str(), "cmd" | "cmd.exe") {
//         return None;
//     }
//     let elements = collect_elements(node, source);
//     let mut start = usize::MAX;
//     let mut parts: Vec<&'a str> = Vec::new();
//     for (i, (kind, text)) in elements.iter().enumerate() {
//         let t = text.trim();
//         if kind == "command_parameter" {
//             let l = t.trim_start_matches('/').to_lowercase();
//             if l == "c" || l == "k" {
//                 start = i;
//                 continue;
//             }
//         }
//         if i > start {
//             if !t.is_empty() {
//                 parts.push(t);
//             }
//         }
//     }
//     if parts.is_empty() {
//         None
//     } else {
//         Some(parts.join(" "))
//     }
// }
//     let elements = collect_elements(node, source);
//     let mut skip_next = false;
//     for (kind, text) in elements {
//         if kind == "command_parameter" || (text.starts_with('/') && text.len() > 1) {
//             // /c /k 等选项
//             skip_next = text.eq_ignore_ascii_case("/c") || text.eq_ignore_ascii_case("/k");
//             continue;
//         }
//         if skip_next {
//             return Some(text);
//         }
//         if text.trim().len() > 1 {
//             return Some(text);
//         }
//     }
//     None
// }
//
// /// 是否为 Windows 物理磁盘/卷设备路径(`\\.\PhysicalDriveN`、`\\.\C:` 等)。
// pub use crate::security::detect::utils::is_ps_physical_device;
//
// /// 文本中是否包含内联 IPv4 字面量(如 '1.2.3.4')。
// pub fn contains_ip_literal(text: &str) -> bool {
//     static IP_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
//         regex::Regex::new(r"\b\d{1,3}(?:\.\d{1,3}){3}\b").expect("valid ip regex")
//     });
//     IP_RE.is_match(text)
// }
//
// /// 提取节点文本为 owned String。
// pub fn node_extract_text_owned(node: &Node, source: &[u8]) -> Option<String> {
//     node_extract_text(node, source).map(|s| s.to_string())
// }