Gh0st-La6z-exe/null-sigma
GitHub: Gh0st-La6z-exe/null-sigma
一个用纯 Rust 编写的高性能 Sigma 规则流式编译与评估引擎,针对大规模安全事件日志实现超高速检测匹配。
Stars: 1 | Forks: 0
# null-sigma
[](https://github.com/Gh0st-La6z-exe/null-sigma/actions/workflows/ci.yml)
[](https://crates.io/crates/null-sigma)
[](https://docs.rs/null-sigma)
[](LICENSE)
一个纯 Rust 编写的 [Sigma](https://sigmahq.io) 规则评估引擎。
只需解析一次 YAML 规则,将其编译为优化的内部表示,
然后针对完整的规则集评估安全事件流,
单核(Apple M4,release 模式)速度可达 **427 000 events/sec × 1 000 rules**。
```
[dependencies]
null-sigma = "0.1"
```
## 快速开始
```
use null_sigma::SigmaEngine;
use std::collections::HashMap;
let yaml = r#"
title: Detect Encoded PowerShell
logsource: {}
detection:
sel:
CommandLine|contains: '-EncodedCommand'
condition: sel
"#;
let mut engine = SigmaEngine::new();
engine.load_rule(yaml).unwrap();
let mut event = HashMap::new();
event.insert("CommandLine".to_string(), "powershell -EncodedCommand SQBFAFgA".to_string());
let matches = engine.evaluate_event(&event); // &self — safe to call concurrently
assert_eq!(matches[0].rule_title, "Detect Encoded PowerShell");
```
```
$ cargo run --example detect_powershell
Loaded rule: d7da0a5c-0001-0000-0000-000000000001
Engine has 1 rule(s)
[ALERT] Matched 1 rule(s)
- Suspicious Encoded PowerShell [high] score=0.70
tags: attack.execution, attack.t1059.001
Batch (2 events):
event[0]: 1 match(es)
event[1]: 0 match(es)
```
## 为什么它很快
该引擎通过四重叠加优化实现了高吞吐量。
基准测试套件可以对每一项优化进行独立测量。
### 1 — 热/冷数据结构分离
多规则引擎中最昂贵的操作是逐规则循环:对于每个加载的规则,尽可能低成本地判断当前事件是否可能匹配。
最简单的方法是将规则的所有信息存储在一个 struct 中并进行迭代。以每条规则 200–500 字节计算,1 000 条规则 = 每个事件需要流式传输 200–500 KB 的数据——每次迭代都会导致 L2/L3 缓存未命中。
相反,引擎将每条规则分为两个区域:
**`RuleHotData`** —— 24 字节,每条规则的每个事件都会触及:
```
struct RuleHotData {
cat_hash: u32, // 4 bytes — FNV-1a hash of logsource.category
prod_hash: u32, // 4 bytes — FNV-1a hash of logsource.product
svc_hash: u32, // 4 bytes — FNV-1a hash of logsource.service
ac_start: u32, // 4 bytes — index into flat AC pattern slice
ac_len: u32, // 4 bytes — number of AC patterns for this rule
fully_ac_covered: bool, // 1 byte — safe-skip gate
// 3 bytes padding → total 24 bytes
}
```
**`CompiledRule`** —— 200–500 字节,仅在规则通过*两个*预过滤器(通常是 1 000 条中的 1 条)时才会被解引用:
```
struct CompiledRule {
rule: SigmaRule, // parsed YAML fields
identifiers: Vec,
conditions: Vec, // compiled boolean AST
ac_pattern_indices: Vec,
has_regex: bool,
}
```
按每条规则 24 字节计算,1 000 条规则占用 24 KB——刚好装入 L1 缓存(Apple M4 为 64 KB)。整个预过滤器扫描只触及这个数组;对于被跳过的规则,绝不会解引用冷数据的 `CompiledRule` 堆内存分配。
logsource 字段在规则加载时使用 32 位 FNV-1a 进行了预哈希,因此每条规则的检查只需进行三次整数比较,而不是三次字符串比较:
```
#[inline(always)]
fn logsource_ok(rule_hash: u32, event_hash: u32) -> bool {
rule_hash == 0 || (event_hash != 0 && rule_hash == event_hash)
// ↑ ↑
// wildcard rule field absent in event
}
```
0 被保留作为“无约束”的哨兵值。通过将 FNV 哈希冲突 `h == 0 → 1` 进行重映射,确保了非零的事件哈希值。
哈希比较只是预过滤器,并非最终定论:通过哈希比较的规则会在冷路径(`LogSource::matches`)中针对**实际的 logsource 字符串**进行再次检查,然后才进行评估。因此,32 位哈希冲突无法将事件路由到错误的规则——这种复查在热路径上没有任何成本,因为它只为已经通过两个预过滤器的规则运行。
**实测结果:** 1 000 条规则,具有错误 logsource category 的事件 →
总计 **912 ns**。所有 1 000 条规则在触及任何 `CompiledRule` 之前就被全部拒绝。
### 2 — Aho-Corasick 批量预过滤器
大多数 Sigma 规则包含一个或多个 `|contains`、`|startswith` 或 `|endswith` 字符串条件。如果事件字段不包含这些字符串中的任何一个,则规则不可能匹配——无需评估完整的条件 AST。
在规则加载时,从每条规则中提取出每个符合 AC 条件的字符串模式,放入一个扁平的 `Vec` 中。编译一个统一的
[`aho-corasick`](https://docs.rs/aho-corasick) 自动机跨越所有这些模式。在评估时:
```
run_ac_scan(event)
for each event field value:
for each match in ac_automaton.find_iter(value):
hits[match.pattern_id] = true // O(n) over text, not O(n × rules)
```
结果是一个以模式 ID 为索引的密集布尔位图。然后,热循环针对每条规则测试该位图的连续切片——没有指针间接寻址,也没有 HashMap 查找:
```
let start = hot.ac_start as usize;
let end = start + hot.ac_len as usize;
if !flat_ac_indices[start..end]
.iter()
.any(|&idx| ac_hits[idx as usize])
{
continue; // rule cannot match — skip cold eval
}
```
**为什么 100 条规则比 1 条规则更快:**
| 基准测试 | 中位数 |
|---|---|
| `single_rule_single_event` | 1.35 µs |
| `100_rules_single_event` | **856 ns** |
无论规则数量多少,AC 自动机都会对事件字段扫描一次。一百条规则共享重叠的字符串词汇表意味着,对 100 条规则进行预过滤的总工作量比简单评估单条规则的条件还要少。
只有当满足两个安全条件时,规则才会进行 AC 预过滤:
1. **每个**标识符组中的**每个**字段条件都符合 AC 条件。
包含 `|re`、`|cidr`、数值比较、`|exists` 或转换修饰符(`|base64`、`|wide`、`|windash`)的规则会绕过预过滤器关卡,并始终进行完整评估。
2. 编译后的条件**不可能在所有标识符均为 false 时触发**。这保护了像 `condition: not selection` 这样的否定条件——一个完全没有命中 AC 的事件恰恰可能是应该匹配的事件,因此这些规则永远不会在 AC 未命中时被跳过。
这两项检查在规则加载时计算一次;违反其中任何一项都会导致漏报,因此它们由专门的回归测试(`ac_prefilter_tests`)强制执行。
### 3 — 预编译 regex 缓存
使用 `|re` (regex) 条件的规则会在 `load_rule` 时将其模式一次性编译到每条规则专属的 `HashMap` 中。在评估时,`match_identifier_with_cache` 会直接查找预编译的对象:
```
match regex_cache.get(&pattern) {
Some(re) => re.is_match(field_value), // O(1) lookup + fast match
None => regex_matches(&pattern, field_value), // fallback compile
}
```
编译也是进行验证的地方:无法编译的 `|re` 模式会**在加载时拒绝整条规则**并返回 `EngineError::InvalidRegex`。无法编译的模式永远无法匹配,因此静默接受它会在没有任何操作员信号的情况下禁用检测。
如果没有缓存,`|re` 规则会在每个事件上调用 `regex::Regex::new()`——每个模式的每次调用都会产生 1–10 µs 的编译惩罚。有了缓存后,100 条规则 × 4 个模式 = 400 次 `is_match` 调用,每次约 88 ns。
**实测:** `100_regex_rules_single_event` → **35.9 µs**
### 4 — 零分配字段扩充
引擎支持 Sigma 字段命名(`CommandLine`)和应用程序规范命名(`command_line`)的事件。`enrich_event_cow` 不会总是克隆事件并扫描所有约 120 个映射条目来添加别名,而是返回一个 `Cow<'_, HashMap>`:
- **`Borrowed`** —— 事件已使用 Sigma 名称 → 无内存分配
- **`Owned`** —— 存在应用程序规范名称 → 只克隆一次,仅插入所需的别名
反向查找是在 `FieldMapping::new()` 时预先构建的,因此扩充操作会迭代 O(n_event) 个条目,而不是 O(n_mappings):
```
for (key, value) in event {
if let Some(sigma_name) = self.reverse.get(&key.to_lowercase()) {
if !event.contains_key(sigma_name.as_str()) {
aliases.push((sigma_name.clone(), value.clone()));
}
}
}
if aliases.is_empty() {
return Cow::Borrowed(event); // hot path — zero allocation
}
```
**实测隔离:**
| 路径 | 中位数 | 内存分配 |
|---|---|---|
| `enrich_event_cow` (Sigma 键) | **193 ns** | 无 |
| `enrich_event_cow` (规范键) | **739 ns** | 一次克隆 |
仅此一项更改就将 `single_rule_single_event` 从 2.08 µs 降低到了 **1.25 µs**——性能提升了 40%——因为该内存分配存在于每个 `evaluate_event` 调用的关键路径上。
## 基准测试摘要

左图是核心特性:增加规则几乎没有成本。
在 1 000 条规则下,每个事件的延迟比朴素的逐规则评估**低 576 倍**,因为预过滤器在触及条件树之前就拒绝了几乎所有的规则。该图表由 `scripts/gen_benchmark_chart.py` 根据下面的 Criterion 中位数生成。
所有数据环境:Apple M4,单核,`cargo bench`(release profile),
Criterion 100 次采样的统计测量。
```
single_rule_single_event time: [1.3463 µs 1.3489 µs 1.3517 µs]
100_rules_single_event time: [855.53 ns 856.34 ns 857.20 ns]
1000_rules_single_event time: [2.3385 µs 2.3405 µs 2.3428 µs]
1000_rules_mixed_field_noise time: [15.916 µs 15.934 µs 15.958 µs]
100_rules_100_events_batch time: [93.203 µs 93.298 µs 93.399 µs]
1000_rules_logsource_mismatch time: [909.78 ns 912.15 ns 915.89 ns]
1000_rules_ac_prefilter_zero_match time: [1.7766 µs 1.7881 µs 1.8083 µs]
100_regex_rules_single_event time: [35.783 µs 35.877 µs 35.975 µs]
enrich_event_cow_sigma_keys time: [192.36 ns 193.29 ns 195.25 ns]
enrich_event_cow_canonical_keys time: [732.02 ns 738.83 ns 752.37 ns]
```
单核推导吞吐量:
| 场景 | Events/sec |
|---|---|
| 1 000 rules, 匹配的事件 | **427 000** |
| 1 000 rules, 错误的 logsource | **1 096 000** |
| 1 000 rules, 正确的 logsource, 无 AC 命中 | **559 000** |
| 100 rules × 100 event 批处理 | **1 072 000** |
这些数据包含了 2026 年 7 月添加的正确性强化(logsource 字符串复查、条件感知的 AC 门控、符合规范通配符转义)
——与强化前相比有 1–7% 的性能损耗,但以此换取了对几类漏报情况的消除。
Criterion 生成的交互式 HTML 报告位于
运行 `cargo bench` 后的 `target/criterion/report/index.html`。
## Sigma 功能覆盖
### SigmaHQ 语料库兼容性
已针对完整的官方 [SigmaHQ/sigma](https://github.com/SigmaHQ/sigma)
规则语料库(2026 年 7 月快照)进行了验证:
| 规则集 | 文件 | 已加载 |
|---|---|---|
| `rules` + `rules-emerging-threats` + `rules-threat-hunting` + `rules-compliance` | 3 745 | **3 745 (100%)** |
| `rules-placeholder` (`\|expand` —— 设计上需要外部占位符目录) | 17 | 0 (文档记录的排除项) |
所有 3 745 条规则在 **~270 ms** 内批量加载到单个引擎中(一次 AC 重建)。
重现方式:
```
git clone --depth 1 https://github.com/SigmaHQ/sigma.git corpus/sigmahq
cargo run --release --example corpus_report -- corpus/sigmahq/rules
```
### 条件语言
条件编译器实现了一个完整的递归下降解析器,具有正确的
运算符优先级(`NOT > AND > OR`):
```
(selection_process and selection_cmdline) and not filter
1 of selection*
all of them
3 of (sel_a, sel_b, sel_c)
```
AST 在加载时对每条规则编译一次,生成一个 `ConditionNode` 树,
每个事件的评估时间为 O(n_identifiers)。
### 值修饰符 (19)
| 类别 | 修饰符 |
|---|---|
| 字符串匹配 | `contains` `startswith` `endswith` |
| 模式 | `re` (+ 标志子修饰符 `re\|i` `re\|m` `re\|s`) `cidr` |
| 量词 | `all` |
| 转换 | `base64` `base64offset` `wide` `windash` |
| 数值 | `gt` `gte` `lt` `lte` |
| 存在性 | `exists` |
| 字段引用 | `fieldref` (与另一个事件字段比较,Sigma v2) |
`fieldref` 将条件值解释为字段*名称*,并比较两个事件字段的值(`ParentImage|fieldref: Image` 在进程执行自身时触发);它与 `contains`/`startswith`/`endswith` 组合使用。
Regex 标志子修饰符必须跟在 `re` 之后(`field|re|i:`)——单独的 `|i` 是解析错误。注意:在本引擎中,默认情况下 `|re` 是大小写不敏感的,因此 `re|i` 是一个无操作确认;`re|m`(多行锚点)和 `re|s`(点号匹配换行符)会改变编译行为。
转换修饰符(`base64`、`wide`、`windash`)在匹配前扩展值集合:
- `base64offset` 生成所有三种 base64 边界偏移变体,并从**两端**修剪不稳定的字符,因此即使值嵌入在较长的 base64 数据流中间,也能被检测到(不仅仅是位于编码数据末尾的值)。
- `windash` 扩展为完整的 Sigma 变体集:`-`、`/`、`–` (en dash)、
`—` (em dash) 和 `―` (horizontal bar) ——可捕获从带有排版连字符的文档中复制粘贴的命令。扩展是双向的:使用任何变体编写的规则都可以匹配所有变体。
- `wide` 将值重新编码为 UTF-16LE。
### 条件形式
```
# 单字符串
condition: selection
# Boolean 表达式
condition: selection and not filter
# 量词
condition: 1 of selection*
condition: all of them
condition: 3 of (sel_a, sel_b, sel_c)
# 多个独立条件(任一满足即触发规则)
condition:
- selection_a
- selection_b and filter
```
### 字段匹配语义
- **大小写不敏感**的字段名称和字符串值(Sigma 规范)
- 字段条件组内的 **AND**
- 标识符内跨组的 **OR**
- 条件内跨值的 **OR**(除非使用 `|all`)
- **关键字搜索**(空字段名)匹配所有事件值
- 值中的**通配符**:`*` (任意序列) 和 `?` (单个字符)
- 根据 Sigma 规范**转义**:`\*` 和 `\?` 是字面量字符,
`\\` 是单个反斜杠,普通字符前的单独反斜杠保持不变
—— `\cmd.exe` 这样的 Windows 路径不需要转义。
转义后的字面量依然符合 Aho-Corasick 预过滤器的条件(自动机存储的是未转义的字节)。
## 类型系统
```
SigmaRule
├── LogSource { category, product, service }
├── Detection
│ ├── ConditionExpr Single(String) | Multiple(Vec)
│ └── identifiers HashMap
│ └── SearchIdentifier
│ └── Vec ← OR across groups
│ └── Vec ← AND within group
│ ├── field: String
│ ├── values: Vec
│ └── modifiers: Vec
└── metadata title, id, status, level, tags, author, …
```
`SigmaValue` 是强类型的:`String | Integer(i64) | Float(f64) | Boolean | Null`。
当双方都能解析为整数时,数值比较使用 `i64` 整数运算,避免了大型计数器和时间戳在 `f64` 下遇到的 2^53 精度边界问题。
## 线程安全
在规则加载后,`evaluate_event` 和 `evaluate_batch` 都使用 `&self`。
引擎可以被包装在 `Arc` 中,并从 N 个线程并发评估,无需加锁:
```
use std::sync::Arc;
let engine = Arc::new(engine_with_rules_loaded);
let handles: Vec<_> = events
.chunks(chunk_size)
.map(|chunk| {
let eng = Arc::clone(&engine);
std::thread::spawn(move || eng.evaluate_batch(chunk))
})
.collect();
```
`rebuild_ac` 调用(Aho-Corasick 自动机编译)是在 `load_rule` / `load_rules` 内部即时(eagerly)执行的——绝不会在 `evaluate_event` 内部延迟执行。
## 测试
```
cargo test
running 196 tests
0 unit (lib)
10 corpus_tests — parse known-good and known-bad YAML fixtures
10 property_tests — proptest: invariants proven on 10,000 random inputs each
174 sigma_tests — modifiers, conditions, engine, hardening, concurrency
2 doc tests
```
由 proptest 证明的部分属性不变量:
- **无 panic** —— 对 `parse_rule` 输入任意可打印字符绝不会引发 panic
- **确定性** —— 相同的事件始终产生相同的结果
- **可靠性** —— 缺失的必填字段绝不会触发规则
- **单调性** —— 额外的事件字段绝不会抑制匹配
- **通配符 logsource** —— 带有空 logsource 的规则匹配任何事件源
精选的强化测试:
- 字段值中的空字节、RTL 覆盖字符和零宽空格
- 10 000 字符的字段值
- 单个事件中的 1 000 个字段
- Unicode 字段名
- 重复的规则加载(均被加载,无静默去重)
- 暴力破解的 FNV-1a logsource 哈希冲突——被证明不会导致事件路由错误
- 否定条件(`not selection`)——被证明绝不会 AC 预过滤器跳过
- 无效的 `|re` 模式——在加载时被拒绝,引擎状态保持不变
## 安全性
- **零 `unsafe` 代码** —— crate 级别强制执行 `#![forbid(unsafe_code)]`
- **无 C 扩展** —— 纯 Rust,无 pyo3,无 bindgen,无 FFI
- **设计上无 panic** —— 格式错误的 YAML 返回 `ParseError`,无效的条件返回 `CompileError`,而无效的 `|re` 模式在加载时返回 `EngineError::InvalidRegex`(响亮地失败,绝不静默失败)
- **无静默的检测丢失** —— 每次加载失败都会使引擎状态保持不变;`load_rules` 收集每条规则的错误,因此批量导入可以准确报告哪些规则被拒绝及其原因
- **`#![deny(missing_docs)]`** —— 所有公开项均已编写文档
- **Clippy pedantic** —— 在 `-W clippy::pedantic` 下零警告
## Cargo 特性
没有可选特性。依赖范围被刻意保持在最小:
| Crate | 作用 |
|---|---|
| `serde` + `serde_norway` | YAML → `SigmaRule`(处于维护状态的 `serde_yaml` 继任者) |
| `aho-corasick` | SIMD 加速的多模式批量扫描 |
| `regex` | `\|re` 修饰符模式匹配 |
## 许可证
基于 [Apache License, Version 2.0](LICENSE) 授权。
版权所有 2026 Gh0st-La6z-exe
标签:Rust, Sigma规则, 可视化界面, 目标导入, 编译器, 网络流量审计, 通知系统