我如何将 647 条 Semgrep 规则编译为原生 Rust
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 上:
| Tool | Time |
|---|---|
| Semgrep | 4 m 12 s |
| RMA | 23 s |
大约 快 10 倍。代码库越大,差距越明显。
仍待完善的地方
- 生成代码的误报(正在改进启发式算法)
- 某些 Semgrep 功能尚未完全支持(污点模式仅部分实现)
- 错误信息可以更友好
试一试
cargo install rma-cli
rma scan .
或使用交互式 TUI:
rma scan . --interactive
采用 MIT 许可证。
期待大家的反馈,尤其是如果你在自己的项目中尝试过。哪些规则缺失?误报太多?请告诉我。
如果你对模式编译器的实现感兴趣,可以查看仓库中的 crates/rules/build.rs。