내가 647개의 Semgrep 규칙을 네이티브 Rust로 컴파일한 방법
Source: Dev.to
나는 Semgrep를 사랑한다. 수천 개의 커뮤니티가 기여한 보안 규칙이 실제 취약점을 잡아준다. 하지만 큰 코드베이스에 실행할 때마다 나는… 그리고 또 기다려야 했다.
문제는? Semgrep는 YAML 규칙을 런타임에 Python으로 해석한다. 500 K 라인 규모의 모노레포에서는 스캔당 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의 ... 연산자는 “아무거나 0개 이상”을 매치한다:
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 파싱도 없으며, 런타임에 패턴을 컴파일할 필요도 없다. 규칙은 그대로 존재한다.
결과
500 K LOC 모노레포에서:
| Tool | Time |
|---|---|
| Semgrep | 4 m 12 s |
| RMA | 23 s |
약 10배 빠른 속도. 코드베이스가 커질수록 차이는 더 커진다.
아직 미흡한 부분
- 생성된 코드에 대한 오탐 (더 나은 휴리스틱을 개발 중)
- 일부 Semgrep 기능이 아직 지원되지 않음 (taint 모드가 부분적으로만 구현)
- 오류 메시지가 더 명확해질 필요가 있음
사용해 보기
cargo install rma-cli
rma scan .
또는 인터랙티브 TUI로:
rma scan . --interactive
MIT 라이선스이다.
피드백을 환영한다. 특히 직접 프로젝트에 적용해 본 경험을 알려줘. 어떤 규칙이 빠졌는가? 오탐이 너무 많은가? 알려줘.
패턴 컴파일러 구현에 관심이 있다면, 레포의 crates/rules/build.rs를 확인해 보라.