我如何将 647 条 Semgrep 规则编译为原生 Rust

发布: (2026年2月6日 GMT+8 06:31)
4 min read
原文: Dev.to

Source: Dev.to

我喜欢 Semgrep。它拥有成千上万的社区贡献安全规则,能够捕获真实的漏洞。但每次在大型代码库上运行时,我都要等……等。

问题出在哪里?Semgrep 在运行时使用 Python 解释 YAML 规则。对于一个 50 万行的 monorepo,这意味着 每次扫描要耗时 4 分钟以上

于是我问自己:如果把这些规则编译成本地代码会怎样?

思路

Semgrep 规则本质上是模式匹配。像下面这样的规则:

rules:
  - id: sql-injection
    pattern: execute($QUERY)
    message: "Possible SQL injection"

的含义是“找出所有对 execute() 的调用且只有一个参数”。这与 Tree‑sitter 使用查询语言进行的匹配并没有根本区别。

如果在构建时把 Semgrep 模式翻译成 Tree‑sitter 查询,嵌入到二进制文件中,然后直接对 AST 进行匹配,会怎样?

难点:元变量

Semgrep 使用 $VARIABLES 来捕获任意代码:

eval($USER_INPUT)

这可以匹配 eval(x)eval(foo.bar)eval(getInput()) —— 任何东西。

Tree‑sitter 查询没有元变量,只有 捕获

(call_expression
  function: (identifier) @func
  arguments: (arguments (_) @arg))

@func@arg 就是捕获——它们会抓取匹配该位置的内容。

于是我实现了一个翻译器。它解析 Semgrep 模式,识别元变量,并在相应位置生成带捕获的 Tree‑sitter 查询。

// Simplified version of the pattern compiler
fn compile_pattern(semgrep: &str) -> TreeSitterQuery {
    let ast = parse_semgrep_pattern(semgrep);
    let mut query = String::new();
    for node in ast.walk() {
        match node {
            Metavar(name) => {
                // $X becomes (_) @x
                query.push_str(&format!("(_) @{}", name.to_lowercase()));
            }
            Literal(text) => {
                query.push_str(&format!("\"{}\"", text));
            }
            // ... more cases
        }
    }
    TreeSitterQuery::new(&query)
}

省略号问题

Semgrep 的 ... 操作符匹配“零个或多个任意内容”:

func($ARG, ...)

这可以匹配 func(a)func(a, b)func(a, b, c, d, e)

Tree‑sitter 查询无法直接表达这种模式。对于这类情况,我退回到手动遍历 AST 并检查结构是否匹配。虽然没有原生查询快,但仍然比 Python 解释快得多。

构建时编译

魔法发生在 build.rs 中。编译期间:

  • 解析所有 647 个 Semgrep YAML 文件
  • 将每个模式翻译成 Tree‑sitter 查询(或 AST 遍历器)
  • 将所有内容序列化为二进制块

然后使用 include_bytes!() 嵌入:

// In the compiled binary
static RULES: &[u8] = include_bytes!("compiled_rules.bin");

// At runtime – instant loading
fn load_rules() -> RuleSet {
    bincode::deserialize(RULES).unwrap()
}

无需文件 I/O、无需 YAML 解析、也不在运行时编译模式。规则就已经“在那儿”了。

结果

在 50 万行代码的 monorepo 上:

ToolTime
Semgrep4 m 12 s
RMA23 s

大约 快 10 倍。代码库越大,差距越明显。

仍待完善的地方

  • 生成代码的误报(正在改进启发式算法)
  • 某些 Semgrep 功能尚未完全支持(污点模式仅部分实现)
  • 错误信息可以更友好

试一试

cargo install rma-cli
rma scan .

或使用交互式 TUI:

rma scan . --interactive

采用 MIT 许可证。

期待大家的反馈,尤其是如果你在自己的项目中尝试过。哪些规则缺失?误报太多?请告诉我。

如果你对模式编译器的实现感兴趣,可以查看仓库中的 crates/rules/build.rs

Back to Blog

相关文章

阅读更多 »