Architecture¶
Source tree¶
src/
├── main.rs # Binary + pipeline (run_pipeline, Stats, AggregatedRule)
├── config.rs # YAML config (serde, Default) with LogConfig
├── logger.rs # Two-layer tracing subscriber (stderr info + daily rolling file debug)
├── sigma/
│ ├── loader.rs # grit-lib clone/fetch + remote URL update + find_rules_dirs()
│ └── engine.rs # SigmaEngine: load rules (post-parse filter), evaluate events, rule evaluation
├── collector/
│ ├── mod.rs # pub mod winevt
│ └── winevt.rs # WinevtCollector (EvtQueryW, EvtNext, EvtRender)
├── evtx/
│ └── writer.rs # write_evtx() via EvtExportLog API (→ valid EVTX or .xml fallback)
├── parser/
│ └── mod.rs # XmlParser (Winevt XML → flat JSON)
└── regression/
├── mod.rs # SkipSet, build_skip_set(), validate_rule_id(), triplet validation
├── generator.rs # RegressionData: aggregate + write output
└── info_yml.rs # InfoYml struct (rule_metadata, regression_tests_info)
Pipeline (single run, sequential)¶
- Load config (create
config.yamlwith defaults if missing) - Create directories:
regression_data/,regression_data/rules/ - Acquire SigmaHQ rules via
grit-lib(clone); exit error if no rules found find_rules_dirs()scanssigma/forrules/rules-*dirs (excludesrules-compliance)- Build skip set by scanning
regression_data/rules/+sigma/regression_data/for existinginfo.yml→HashSet<String>of rule IDs - Load Sigma rules from all
rules*dirs, excluding skipped rule IDs; post-parse filter viarule.logsource.productfilters non-Windows rules; status/level filter viaconfig.sigma.min_status/min_level(sole allowed optimization) — a startup rule table is displayed (loaded / skipped / active services) - Collect events via
WinevtCollector(channels from config) →Vec<WinevtEvent>: - Each event carries
event_json: Option<Value>(pre-parsed by collector, XmlParser fallback if None) - Each event's
LogSourceis derived from the channel viaresolve_logsource(channel → service > provider → service > default) - Evaluate against all loaded rules via
evaluate_event_with_logsource(event, logsource)— no event lost - Aggregate matches by
rule_idinHashMap<String, AggregatedRule> - Generate regression for rules without existing
info.yml(skip at generate time too) - Write:
<output>/<rule_rel_path>/<rule_id>.json(first matched event) +<rule_id>.evtx+info.yml; appendregression_tests_pathline to the source rule YAML
Architectural invariants (non-negotiable foundations)¶
- 100% sequential pipeline: acquire rules → load engine → collect events → match → generate
- All in RAM: in-memory aggregation before writing (no intermediate DB)
- One run = complete cycle (no "just collect" or "just generate" mode)
- Windows collection via Winevt API (
windowscrate,EvtQueryW/EvtNext/EvtRender) — no ETW, no ferrisetw - Output =
regression_data/<rule_rel_path>/(triplet:<rule_id>.json+<rule_id>.evtx+info.ymlSigmaHQ format) - Real-time engine:
rsigma-evalloaded once with all non-skipped rules; every event is evaluated against all loaded rules. No event lost. Skip-at-load is the only optimization. - LogSource derived from channel ETW (
resolve_logsource), with provider as fallback. - Priority: channel → service > provider → service > default
- See
# INVARIANT:comment insrc/sigma/mapping/mod.rs - EVTX via
EvtExportLog: re-queries the event by RecordID from the live log. On success → binary.evtx. On failure (event rotated out of retention) or non-Windows →.xmlfallback (raw XML, not invalid binary). - Known limitation: race condition with log retention — if the event has been purged between collection and export,
EvtExportLogfails silently (ERROR_EVT_QUERY_RESULT_STALE).
Skip set details, key design decisions, and skip set construction logic are in
architecture-reference.md(Stages 2, 5, 6, 7).