TL;DR
RSigma v0.21.0 is the "daemon throughput and STIX interop" release: the evaluation and daemon path gain witness-based candidate indexing, parallel parsing, multicore batching, and jemalloc on musl; rstix completes OASIS STIX 2.1 Interoperability use-case coverage with a self-certification CI gate; and detection engineering gains verified rule tuning, OCSF findings, and post-pipeline rule inspection.
- Daemon throughput: jemalloc for the musl allocator (#412, thanks to @alltilla), parallel batch parsing and single-parse DLQ (#415), default multicore batching (#408), witness-based candidate indexing with pre-filter soundness fixes (#406), concurrent multi-batch detection (#426, #428), short-circuit condition evaluation (#427), ASCII case-insensitive Aho-Corasick and top-level key probes (#420, #419), plus representative performance baselines and cross-architecture gates (#404, #409, #429).
rstixSTIX interop: OASIS CSD01 golden harness through all 21 use cases (#403, #410, #413, #433, #438), self-certification CI gate (#440), STIX 2.1 wire conformance (#388), and TAXII collection ingest with DANE DNSSEC (#387, #381), thanks to @SecurityEnthusiast.- Detection engineering: verified false-positive-driven
rule tune(#431), post-pipeline rule retrieval for embedders (#437), OCSF Detection Finding output (#397), incident bundle export (#400), and list-valuedadd_condition(#399, thanks to @frack113). - Operator surfaces: per-sink output formats (#396), complete
--output-formatcoverage (#389), and a documentation accuracy overhaul (#435). - Dependencies: Dependabot batches across Rust, CI actions, and the VS Code extension (#402, #432, #444).
Dependency batch (Aug 2026 vscode) (#444)
VS Code extension transitive security updates in editors/vscode: undici 7.29.0, brace-expansion 5.0.9, and fast-uri 3.1.5, with npm overrides floors raised to match.
rstix: STIX interop self-certification CI gate (#440)
CI runs a dedicated rstix STIX interop self-certification job that executes the full interop suite (every TESTED manifest row), stamps generated_at into summary.json, fails on a missing/stale/incomplete report via scripts/interop-report-gate.py, and uploads target/interop-report/ (CSD01 §4.2 Tables 55/56, traceability CSV, risks). The gate validates artifact content (checklist cell results, traceability CSV row order and outcomes) against committed gate-expectations.json, not only file presence and row counts. Export invariants in the harness panic before writing a hollow report. That package is the operational SXP/SXC self-certification evidence against Interoperability CSD01 — not an OASIS-issued certificate, and not a §4.1 product-persona claim.
rstix: OASIS §3.3–§3.21 interop use-case tests (#438)
Completes the remaining OASIS STIX 2.1 Interoperability CSD01 use-case modules beyond §3.1 Attack Pattern and §3.2 Campaign. Adds Producer/Consumer/example tests and manifest TESTED rows for Confidence, Course of Action, Data Markings, Grouping, Indicator, Infrastructure, Intrusion Set, Location, Malware Analysis, Malware, Note, Observed Data, Opinion, Report, Sighting, Threat Actor, Tool, Versioning, and Vulnerability (21/21 use cases; 506 TESTED rows: 9 harness + 18 §2.3 + 479 use-case). REQ-CHK-SXP-3.12 and REQ-CHK-SXP-3.16 stay BLOCKED on published defects 19 and 16. Not OASIS SXP/SXC certification.
Also extends the public query surface: QueryableStixObject::get_field now answers ["created_by_ref"] on Infrastructure, IntrusionSet, Location, Malware, MalwareAnalysis, Report, and Vulnerability (completing coverage across all 19 SDOs that implement the trait), and IntrusionSet additionally answers description, resource_level, and primary_motivation.
Post-pipeline rule retrieval (#437)
Pipelines rewrite a rule before it is compiled, and loading the rule kept only the compiled form, so the rewritten Sigma AST was reachable only by hand-rolling apply_pipelines_with_state around a clone. An embedder that derives runtime behavior from the rewrite, such as choosing which Windows event channels to collect from a logsource that change_logsource unified onto the sysmon service, had to duplicate the pipeline's own mapping in its code to get there.
rsigma-eval now exposes transform_rule and transform_collection, returning a TransformedRule with the rewritten rule, the sorted ids of the transformations that fired, and the merged PipelineState; Engine::transform_rule and Engine::transform_collection do the same over an engine's configured pipelines without loading anything. Both are inspection-only siblings of the load path: the load and evaluation paths are unchanged, no rule AST is retained on the engine, and an engine that never calls them costs exactly what it did before. PipelineState is now re-exported from the crate root.
The pipelines guide gains an "Inspecting the rewritten rule" section covering the CLI (pipeline diff) and library routes, the closing change_logsource that makes a routing decision readable off logsource.service, and the load-time-only cost. The eval library page carries the collector example and the builtin pipelines reference explains why the sysmon pipeline ends in change_logsource. The guide also correctly lists three builtin pipelines rather than two.
Corrects the ordering contract documented on the surrounding pipeline functions. apply_pipelines, apply_pipelines_with_state, and apply_pipelines_to_correlation walk the slice they are handed and never sort, so they no longer claim to run "in priority order"; each points at merge_pipelines instead, whose own docs no longer claim to apply pipelines or combine them into one, since it only sorts a slice in place. Every in-repo caller already sorts first, so this is a documentation correction rather than a behavior change. Pipeline::apply_to_collection and transform_collection now cross-reference and spell out how they differ: the former takes one pipeline and shares a single PipelineState across the collection, the latter takes several and scopes applied_items and state to each rule.
Documentation accuracy overhaul (#435)
Aligns the published docmd site with current CLI flags, daemon auth and HTTP behavior, backends, feature flags, crate surfaces, and contributor workflows. Guides, CLI, library, reference, deployment, editors, ecosystem, and developers pages drop roadmap wording, correct dynamic-source --source / --source-file wiring, builtin pipelines, exit codes, NATS/OTLP/Helr examples, and WASM ABI status, and refresh testing and fuzzing inventories against CI.
rstix: OASIS §3.2 Campaign interop use-case tests (#433)
- Adds
tests/interop/use_cases/campaign/covering §3.2.1–§3.2.6 against normative §3.2.3 fixtures and non-gatingexamples/campaign/data. - §3.2.1 description scope is TESTED: normative fixtures expose typed Campaign “Green Group Attacks Against Finance”.
- Producer
REQ-3.2-P-01..P-11, ConsumerREQ-3.2-C-01..C-05, checklistREQ-CHK-SXP-3.2/REQ-CHK-SXC-3.2, examplesREQ-3.2-EX-4.1/EX-4.2. - Table 4 CSD01 typo (
type=threat-actor) is documented; tests enforce STIX §4.2campaign. - Tightens the equivalent §3.1 Attack Pattern rows in the same pass:
P-02pins that caller selection renames exactly one object and that the selected name survives parse and re-validation, andP-07reads the id from the wire use-case object so its parse no longer restates a parse the typed lookup already performed. The RFC 3339 millisecond check both use cases share now lives intests/interop/common/timestamp.rs. - Not OASIS SXP/SXC certification (2/21 use cases).
Dependency batch (Aug 2026) (#432)
Rolls up four open Dependabot PRs into a single merge. Rust (workspace Cargo.lock, with ci/wasm-smoke/Cargo.lock and fuzz/Cargo.lock synced for the base64 bump): the patch group (#422) updates pest/pest_derive 2.8.7 to 2.8.8, serde_json 1.0.150 to 1.0.151, jsonpath-rust 1.0.5 to 1.0.6, tokio-stream 0.1.18 to 0.1.19, time 0.3.53 to 0.3.54, jsonschema 0.48.1 to 0.48.5, clap 4.6.2 to 4.6.4, and rustls-pki-types 1.15.0 to 1.15.1; standalone updates move base64 0.22.1 to 0.23.0 (#423) and toml 0.8.23 to 1.1.3+spec-1.1.0 (#424). CI (all repinned by commit SHA, batched via the actions-updates group, #421): actions/checkout v7.0.0 to v7.0.1, taiki-e/install-action v2.83.3 to v2.85.0, docker/login-action v4.4.0 to v4.5.0, github/codeql-action/upload-sarif v4.37.1 to v4.37.3, and zizmorcore/zizmor-action v0.6.0 to v0.6.1. Held back: rusqlite 0.40.1 (#234) requires Rust 1.89 while the workspace MSRV is Rust 1.88; tikv-jemallocator 0.7.0 (#425, jemalloc 5.3.1) regresses musl routed daemon throughput about 4-7% versus 0.6.1 (jemalloc 5.3.0) in same-host scripts/perf/image-compare.sh runs while still scaling about 4x from one to six rayon threads.
Verified false-positive-driven rule tuning (#431)
rsigma rule tune now contrasts false-positive events with a required true-positive corpus and proposes a standard Sigma filter rule. It verifies every label against the unfiltered target, rejects malformed corpus records, applies the emitted filter through the production engine path, and refuses any separator that suppresses a true positive. Stable value forms, wildcard escaping, and deterministic YAML emission are shared with rule draft; selections require stable context from at least two fields by default, disjoint benign patterns may become supported multi-selection filters, and minimum cluster support prevents single-event memorization. Pipelines transform the target before profiling so emitted fields and logsource match deployment. An optional existing backtest expectations file adds before/after counts, existing bounds, and scoped FP/TP entries to insert under its expectations key. The MCP server exposes the same verified workflow through tune_rules, with rule and pipeline paths confined to --rules-dir and nested directory symlinks rejected. The detection-loop and architecture diagrams now include the tuning workflow and its shared evaluator and MCP surfaces.
Sustained-throughput tuning walkthrough (#430)
The performance guide now provides one end-to-end recipe for sizing rayon, enabling logsource routing, batching sustained traffic, selecting the detection in-flight depth, generating representative HTTP load, and distinguishing engine-only benchmarks from production sink capacity. It also consolidates the measured gains and the final 61.7% eight-worker efficiency result.
Cross-architecture artifact scaling gate (#429)
The weekly/manual performance workflow now compares detection depths 4 and 5 on dedicated eight-core amd64 and arm64 runners for both native glibc release binaries and static musl image binaries. Alternating five-run samples gate on a 0.98 throughput ratio and no more than a 0.8 percentage-point increase in backpressure. The first four-row validation retained depth 5 with throughput ratios from 0.9985x to 1.0201x and lower backpressure in every row.
Deeper concurrent detection pipeline (#428)
Detection-only daemons with eight or more rayon workers now keep five batches in flight by default instead of four. The sequence-numbered reducer continues to restore sink and ack order, correlation engines remain single-batch, and RSIGMA_DETECT_INFLIGHT still overrides the default up to 8. On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, eight rayon threads, and 16 k6 VUs, order-balanced same-machine runs improved median throughput from about 665k to 708k events/s.
Short-circuit nonmatching condition evaluation (#427)
Rule evaluation now decides the condition result before collecting matched-selection details. any and threshold selectors stop once their result is known, while the detail pass runs only for matching rules. On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, and 16 k6 VUs, order-balanced same-machine runs improved median throughput from about 128k to 129k events/s at one thread and from about 643k to 650k events/s at eight threads.
Concurrent detection for correlation-free engines (#426)
When the loaded rule set has no correlation rules, the daemon may evaluate more than one input batch at a time. Detection-only and routed engines share the engine under a read lock; a sequence-numbered reducer restores sink and ack order before dispatch. Correlation engines stay single-batch and exclusive. The default in-flight depth scales with the rayon pool (1 on a single worker, up to 5 on eight or more) and can be overridden with RSIGMA_DETECT_INFLIGHT (capped at 8).
On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, and 16 k6 VUs, the median of three same-machine runs at eight threads moved from about 520k events/s with one in-flight batch to about 615k with two and about 654k with four, against a one-thread median of about 138k events/s on the same build.
Candidate index ASCII case-insensitive Aho-Corasick (#420)
NeedleSet automata now build with ASCII case-insensitive search and prefer a DFA, so ASCII event strings are scanned without allocating a lowercase copy. Keyword probing visits string values in place instead of collecting them into a Vec, field probes that need both exact and substring witnesses fold once and reuse the folded bytes, and hot index maps use ahash instead of SipHash. The daemon evaluates the next batch while the previous batch's sink/ack dispatch runs, so rayon workers are not parked for the whole dispatch phase.
On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, and 16 k6 VUs, the median of three same-machine runs moved from about 102k to 118k events/s at one thread and from about 404k to 467k events/s at eight threads versus the post-#419 baseline on the same host.
Candidate index probes from event top-level keys (#419)
The candidate index no longer walks every indexed field on each event. When an event can report its top-level keys (Event::top_level_keys), the index probes only witnesses under those keys, so sparse payloads skip the miss storm over thousands of absent Sigma fields. JsonEvent::get_field also returns early for dotted paths whose first segment is absent at the root, avoiding path parsing on the common miss.
On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, and 16 k6 VUs, the median of three same-machine runs moved from about 93k to 102k events/s at one thread and from about 386k to 404k events/s at eight threads versus the post-#416 baseline on the same host.
Batch phase timing for post-parse serial regions (#416)
rsigma_batch_phase_duration_seconds now records decode_merge, observe, result_merge, and dispatch alongside parse and evaluate, so operators can rank the remaining ordered-batch costs after parallel parsing. scripts/perf/baseline-daemon.sh prints every phase delta and its share of measured batch time.
rstix: tighten §3.1 Attack Pattern interop evidence (#418)
Closes soft gaps left after #413/#414: EX-4.2 pins published truncated wire ids and requires bundle parse to carry the same InvalidUuid detail those ids share; P-04 scopes Zero diagnostics by structured object_id only; C-04 keeps get_field wire equalities and pins leaf-list cardinality so KillChainPhase/ExternalReference validate loops run.
Parallel daemon parsing and single-parse DLQ routing (#415)
The daemon now parses each formatted input batch in parallel before taking the engine mutex. A separate ordering lock keeps exactly one batch in flight, so raw taps still see every line before parsing, decoded taps and observers retain input order, correlations run in order, output order is unchanged, and an in-flight batch remains attached to the engine snapshot it started with. Hot reload and correlation-state import/export coordinate with the same boundary.
The processor now reports failed input indices with its batch result. DLQ-enabled daemons use those results instead of parsing every event once to validate it and again to evaluate it, so valid and malformed events each pass through the format parser once. Daemons without an event filter also avoid the former identity filter's JSON conversion and clone.
On the pinned SigmaHQ raw Windows workload with --logsource-routing, --batch-size 512, and 16 k6 VUs, the median of three same-build runs moved from 306,351 to 384,906 events/s at eight threads while one-thread throughput remained near 90,000 events/s. Eight-core scaling improved from 3.39x to 4.19x, or from 42% to 52% efficiency, and the measured parse share fell from about 40% to 27%. A zero-copy request-body span prototype did not improve throughput and was not retained.
rsigma_batch_phase_duration_seconds{phase="parse"|"evaluate"} exposes cumulative batch time for the two measured regions. scripts/perf/baseline-daemon.sh reports their deltas and parse share after each run.
jemalloc as the musl global allocator (#412)
The released static binary is built against musl, whose mallocng serializes the concurrent allocation that Engine::evaluate_batch performs on every batch. That erased the daemon's multicore scaling in the released artifact: on the pinned SigmaHQ corpus with --logsource-routing the daemon reached 53,435 events/s across six cores while the same binary evaluated 57,463 events/s on one, so the fan-out returned less than nothing. musl targets now use jemalloc as the global allocator, which raises that daemon figure to 160,238 events/s on fewer cores (3.00x) and 95,505 from 36,875 without routing (2.59x). Single-threaded throughput moves only ~14%, and that disproportion is what identifies the cost as allocator contention rather than per-event work. Match counts are identical either way. Rule loading, being allocation-heavy, drops from 0.86 s to 0.49 s. glibc and macOS builds keep the system allocator since neither shows the contention, so nothing changes for a native build. Building the musl target now needs a C toolchain for jemalloc-sys, so the Docker builder installs build-base. Measured with scripts/perf/baseline-eval.sh and the new scripts/perf/image-compare.sh, which sees a class of change the native harnesses cannot.
rstix: faithful STIX id errors across the serde boundary (#414)
ParseError::InvalidStixId now reports the defect in the id that was actually rejected. Recovery previously rebuilt StixIdError::InvalidUuid by re-parsing a fixed stand-in id, so the recovered error, and the STIX-E0003 diagnostic built from it, described the stand-in instead of the input: an id whose final UUID group is short was reported as though it had the wrong number of groups. The tagged payload now carries the rejected id and recovery replays StixId::parse, which reproduces the original error, uuid crate detail included. Ids longer than 256 bytes are left untagged rather than echoed back, so a hostile document cannot inflate an error message by the length of its own input.
Recovery no longer panics. The stand-in path asserted that a hardcoded id still failed in the expected way, on a path reachable from parsing untrusted documents; malformed payloads now fall back to ParseError::Json.
A wrong type prefix on a typed reference is now reported as such. created_by_ref and the other typed reference properties reject a mismatched id through StixIdError::TypeMismatch, which crossed the serde boundary as an opaque message and surfaced as ParseError::Json with a generic STIX-E0001 diagnostic. Parsing a bundle whose created_by_ref holds, say, a malware-- id now yields ParseError::InvalidStixId(StixIdError::TypeMismatch { .. }), and the pipeline reports STIX-E0021 (wrong target type for a typed reference) naming the type the property requires. Recovery resolves the expected type through StixObjectKind rather than allocating a &'static str, so it needs neither the leak the previous reconstruction required nor a change to the error type. StixObjectKind::as_str is public for that lookup.
rstix: OASIS §3.1 Attack Pattern interop use-case tests (#413)
- Adds
tests/interop/use_cases/attack_pattern/covering §3.1.1–§3.1.7 against normative §3.1.3 fixtures and non-gatingexamples/attack-pattern/data. - §3.1.1 description scope is TESTED (not
REPORT_ONLY): normative fixtures must expose typed Attack Pattern TTPs including the doc’s “Spear Phishing” example. - Producer
REQ-3.1-P-01..P-13, ConsumerREQ-3.1-C-01..C-05, checklistREQ-CHK-SXC-3.1/REQ-CHK-SXP-3.1, examplesREQ-3.1-EX-4.1/EX-4.2/EX-7.1. ex-3.1.4.2retains published truncated UUID ids (defect 21) and asserts parse rejection — no invented digits.- Each requirement row carries evidence distinct from its siblings:
P-04runsAttackPattern::validateplus a strict bundle report instead of repeating theP-01gate,C-04reads fields throughQueryableStixObjectand validates typed leaves, andREQ-CHK-SXC-3.1closescreated_by_refto a typed Identity rather than re-validating producer data.P-05asserts the wiretype,P-11andP-12require exactly three subsecond digits, andEX-4.2matchesParseError::InvalidStixId(InvalidUuid)instead of substring-matching the error text. - Adds
ParseError::InvalidStixIdso truncated or otherwise invalid STIX ids survive the serde boundary as typed errors (same tagging approach asModelError). - Not OASIS SXP/SXC certification (1/21 use cases).
rstix: OASIS STIX 2.1 Interop normative fixtures and §2.3 suite checks (#410)
- Adds normative OASIS interop test-case JSON under
tests/fixtures/interop/testcases/with provenance sidecars for the §2.3 cross-cutting and §3.x use-case inventory (44 gating fixtures; 42 exercised in the suite-wide walk; 2 recorded asBLOCKEDon unrepairable OASIS §9.1 publisher defects). - Runs §2.3 cross-cutting manifest rows suite-wide over walkable fixtures through the interop bundle gate, per-type producer use-case rules, wire preservation on re-serialize, and expanded consumer, identity, relationship, and SCO checks.
ParseOptions::interop_bundle()andexempt_predefined_tlp_marking_refsextend bundle parse and reference validation for interop TLP marking fixtures;Validator::interop_bundle_strict()adds a bundle-scoped strict profile for the harness.- Breaking:
Grouping.nameis nowOption<String>at parse, matching STIX §4.4 (onlycontextandobject_refsare required on grouping). Callers that constructed or readGrouping.nameasStringmust handleOption/ provideSome(...)when building test fixtures.
Cased substring bloom soundness (#411)
The bloom prefilter now normalizes ASCII |cased substring needles into the same comparison space used for event probes, preventing uppercase patterns such as CMD.EXE from being rejected before evaluation. Non-ASCII cased needles conservatively disable the field's bloom filter because context-sensitive Unicode lowercasing cannot preserve every substring relation.
Representative performance regression gates (#409)
Representative SigmaHQ performance now has a checked-in CI contract. Pull requests that touch the evaluator or performance harness build the PR and its base revision on the same runner, run the same load-corrected median-of-three offline matrix, verify match counts, and reject a head/base throughput ratio below 0.5. The deliberately coarse floor catches roughly 2x regressions without treating shared-runner variance as a precise benchmark. Weekly and manually dispatched runs report bootstrap 95% confidence intervals over five samples and retain the full offline/daemon matrices and raw environment data for 90 days.
The daemon matrix now measures --include-event with and without logsource routing, including a match-heavy lane, uses a standard-library Python load driver when k6 is unavailable, and includes checked-in event-count/value-count/value-sum/temporal-ordered correlation rules/events because the pinned SigmaHQ corpus contains no correlation rules. A corpus test measures the production CandidateIndex directly and enforces p95 candidate sets below 10% of loaded rules. The measured p95 is 0.29-3.86% across the deterministic lanes, confirming the criterion that was previously supported only by the witness-audit simulation.
Multicore daemon batching by default (#408)
The daemon now processes up to 128 queued events per batch by default instead of one. A batch is the unit that detection fans across rayon, so the old default left detection single-core unless operators set --batch-size explicitly. The daemon does not wait to fill a batch: it blocks for the first event and then drains only the events already queued, up to the configured limit.
Explicit --batch-size and daemon.input.batch_size settings continue to override the default. The effective batch size is capped at --buffer-size, preventing configurations where a batch can never be filled by the bounded input queue. Lower batch sizes reduce the maximum processing quantum for latency-sensitive bursts; higher values can improve sustained throughput on large rule corpora. The published SigmaHQ throughput measurements continue to use 512.
Witness-based candidate indexing (#406)
The candidate index could only pre-filter on exact field values, so any rule built from substrings, keywords, regexes, or numeric comparisons was evaluated against every event. On the SigmaHQ corpus that left 79% of rules always-evaluated, which is why per-event cost tracked the total rule count rather than the rules an event could plausibly match.
Rules are now analyzed into witnesses: necessary conditions, at least one of which holds on every event the rule can match. The index inverts that relation across a presence list and an exact-value map per field, one Aho-Corasick automaton per field over that field's substring needles, and one automaton over keyword needles matched against every string value in the event. Rules whose conditions admit no witness, principally anything negated, remain always-evaluated.
Measured on the SigmaHQ corpus at the pinned CI SHA (3,132 rules, Apple M4 Pro), with match counts identical before and after in every cell. Offline figures are single core over 100,000 events and net of the ~0.3 s rule load; daemon figures are HTTP end-to-end at --batch-size 512 with four concurrent posters, read from rsigma_events_processed_total:
| Lane | Offline before | Offline after | Daemon before | Daemon after |
|---|---|---|---|---|
| raw_windows | 2,601 | 33,884 | 20,193 | 195,639 |
raw_windows --logsource-routing |
3,556 | 98,836 | 25,904 | 342,070 |
| structured_windows | 1,112 | 19,358 | 8,868 | 87,704 |
structured_windows --logsource-routing |
1,695 | 29,800 | 12,550 | 113,601 |
| cisco_syslog | 2,917 | 66,434 | 21,715 | 287,000 |
| sysmon_file_event | 1,954 | 26,836 | 14,647 | 126,001 |
--cross-rule-ac changed sides. It used to earn its keep on top of --logsource-routing; now routing alone beats every AC configuration on all four lanes both offline and end-to-end, and adding AC costs 20-35%. The candidate index does the same substring work over a smaller rule population. The flag stays correct and feature-gated but is no longer recommended, and the tuning guide says so.
scripts/perf/baseline-eval.sh now reports throughput net of rule load (#407) and runs 100,000 events per lane instead of 10,000. Load is a fixed ~0.3 s startup cost that accounted for roughly half the wall time of a single 10k pass, so including it understated real throughput by about 2x and compressed the apparent improvement. scripts/perf/baseline-daemon.sh gained an RSIGMA override so a pre-change build can be measured with the same harness, and scripts/perf/daemon-matrix.sh walks the lane and flag matrix end-to-end.
Three behavior changes:
- The
rsigma_eval::rule_indexmodule and itsRuleIndextype are gone, replaced by an internalCandidateIndex. The index was never usable on its own (it indexedCompiledRuleslices the engine owned), so callers should reach forEngineinstead. - Candidate rules are now returned in ascending rule order, so a match set no longer depends on hash iteration order. Consumers that relied on the previous incidental ordering will see a different, and now stable, order.
Engine::logsource_pruned_totaland the daemon'srsigma_rules_pruned_by_logsource_totalcounter now count both sources of conflict pruning: always-evaluated rules the index's product partitioning skips, and candidate rules the residual logsource check drops during evaluation. Witness indexing shrinks the always-evaluated population the counter previously tracked on its own, so counting only that population would have made the metric report near-zero.
Pre-filter soundness fixes and a full-scan differential oracle (#406)
Fixes four cases where a pre-filter silently dropped a rule that should have matched, so an affected rule produced no detection at all. Found by a new differential oracle, Engine::evaluate_full_scan, which evaluates every loaded rule with no candidate index, no cross-rule Aho-Corasick mask, and no bloom, and is therefore authoritative: a pre-filter may only ever over-approximate the candidate set. The rsigma-eval test suite now compares the two paths across a per-witness-class rule battery, a randomized rule and event generator, and an opt-in corpus run over a real rule tree and NDJSON event lanes (RSIGMA_DIFF_RULES / RSIGMA_DIFF_EVENTS).
- The candidate index decided indexability from the rule's detections alone and never looked at its condition. A rule such as
condition: selection or not filterwhose detections are all exact-valued was indexed, so an event carrying none of those values was never a candidate even though the negated branch made the rule fire. Rules with a negated condition are now always evaluated. This affects the default configuration with no flags set. The interim fix is deliberately conservative and also covers the commonselection and not filtershape, which stays reachable through its positive conjunct; the witness-based index restores pruning for it. --cross-rule-acindexed|casedneedles in their original case while scanning a lowered haystack, so the needle could never be found and every rule built only from cased substring matchers was pruned on every event. Needles are now lowered on insertion, and a cased needle outside ASCII disqualifies its rule from pruning because Rust's lowering of a Greek capital sigma is position-dependent and can break the needle's contiguity in the lowered haystack.--cross-rule-acfed only plain string field values through the automaton and skipped arrays, numbers, and bools, leaving the rule's hit bit clear, which the engine reads as "drop". The automaton now sees the same string projections the matcher does, including each array member.--bloom-prefilterinserted no bits for needles shorter than the three-byte trigram window, so a probe could answerDefinitelyNoMatchfor a haystack that did contain the needle. A field carrying such a needle now gets no filter at all, on both the batched build and the incremental append path.
Vendor-shape performance lanes and prefilter composition guidance (#405)
- Two new deterministic event lanes in
scripts/perf/gen_events.py, modeled on real collector output:cisco_syslog(raw Cisco AAA command accounting in a singlemessagefield withproduct/servicehint fields) andsysmon_file_event(Sysmon EventID 11 FileCreate withproduct/categoryhints and a stringEventID, as some forwarders emit it). - The new lanes show that
--logsource-routingand--cross-rule-acdo not always compose: events whose logsource hints route to a small rule subset are faster with routing alone (21.3k vs 14.8k events/s per core on the Cisco lane, 269k vs 219.5k end-to-end in the daemon).BENCHMARKS.mdand the performance tuning guide carry the measured rows. Witness-based candidate indexing (below) supersedes the guidance these lanes produced:--logsource-routingalone now wins on every lane, so the composition question no longer depends on how narrowly the traffic is tagged. - Fixed the executable bit on
scripts/perf/baseline-eval.sh, which was committed non-executable.
OASIS STIX 2.1 Interop golden harness (#403)
- Adds the golden test harness for OASIS STIX 2.1 Interoperability (
stix-2.1-interop-v1.0-csd01):tests/interop/target with a custom runner (harness = false;validate+marking+graph),tests/interop_sentinel.rssilent-skip guard,tests/fixtures/interop/manifest.toml, provenance-aware fixture loader, overlay onValidator::interop_strict()(SHOULD-levelSTIX-I0002downgrade), bundle-closure checker with TLP exemption, containment helpers,linkmeself-registration inventory, and certification report generation totarget/interop-report/. Harness smoke checks for §2.3 cross-cutting layout are recorded separately from OASIS-verified requirements; full normative test-case fixtures follow in subsequent interop work.
Representative performance baseline and corpus witness audit (#404)
- New reproducible performance fixtures:
scripts/perf/fetch-fixtures.shpins the SigmaHQ corpus to the CI SHA andscripts/perf/gen_events.pydeterministically generates six event lanes (raw Windows blobs, structured Windows, mixed schema, no-match, low-match, match-heavy).scripts/perf/baseline-eval.shruns the offline eval flag/thread matrix andscripts/perf/baseline-daemon.shmeasures daemon HTTP end-to-end throughput from the daemon's own processed-events counter. - New
witness_auditexample inrsigma-eval(cargo run --release -p rsigma-eval --example witness_audit): analyzes a rule corpus at the HIR level and reports what fraction carries a sound required-positive witness (exact value, substring, keyword, regex or encoded literal, field presence), the fail-open remainder, and simulated per-lane candidate rates. On SigmaHQ at the pinned SHA: 20.8% of rules are indexable under the current exact-only index, 99.6% carry a sound witness, and simulated candidate rates are 2-3% of loaded rules on Windows-shaped traffic. BENCHMARKS.mdnow separates synthetic microbenchmarks from representative corpus numbers: a new "SigmaHQ corpus baseline" section records measured offline and daemon throughput (roughly 1-4k events/s per core offline and 9-40k events/s for the daemon on a 12-core machine, depending on lane and flags), and the synthetic single-event table now says why its flat rule-count scaling does not transfer to real corpora. The performance-tuning and testing docs pages carry the same correction. Those first offline figures included the fixed ~0.3 s rule load in a 10k-event pass, which understated per-event throughput by roughly 2x; the harness and the published numbers were corrected as part of the witness indexing work below.
Dependency bumps (#402)
Rolls up six open Dependabot PRs into a single merge. Rust (workspace Cargo.lock): the patch group (#398) updates globset 0.4.18 to 0.4.19, serde 1.0.228 to 1.0.229, thiserror 2.0.18 to 2.0.19, async-trait 0.1.89 to 0.1.91, jsonpath-rust 1.0.4 to 1.0.5, futures 0.3.32 to 0.3.33, rustls 0.23.41 to 0.23.42, anyhow 1.0.103 to 1.0.104, and clap 4.6.1 to 4.6.2; standalone updates move sha2 0.10.9 to 0.11.0 (#384), secrecy 0.8.0 to 0.10.3 (#385), and uuid 1.23.5 to 1.24.0 (#386). CI (all repinned by commit SHA, batched via the actions-updates group, #382): taiki-e/install-action v2.83.2 to v2.83.3, EmbarkStudios/cargo-deny-action v2.0.20 to v2.1.1, github/codeql-action/upload-sarif v4.37.0 to v4.37.1, actions/setup-node v6.4.0 to v7.0.0, and zizmorcore/zizmor-action v0.5.7 to v0.6.0. VS Code extension: brace-expansion 5.0.7 to 5.0.8 (#390). Held back: rusqlite 0.40.1 (#234) requires Rust 1.89 while the workspace MSRV is Rust 1.88.
Reject unknown alert-pipeline fields (#401)
- Alert-pipeline YAML now fails to load when any mapping contains an unknown field, including nested scope, dedup, grouping, caps, inhibition, silence, and matcher blocks. A misspelling such as
group.waitpreviously selected the defaultgroup_wait: 30swithout warning, making the daemon run with different timing than the operator configured.
Incident bundle export (#400)
- One incident can be exported as a self-contained report joining it to the ADS documentation of every rule that contributed and the risk entities it overlaps:
rsigma engine incidents export <ID> [--bundle-format json|markdown] [-o PATH], backed byGET /api/v1/incidents/{id}/bundle. An incident records only rule keys and counts, so previously nothing in it said what the rules looked for or why they mattered. - Each rule in a bundle reports how its key resolved against the currently loaded rule set:
unique,ambiguouswhen several loaded rules carry it with differing documentation (a routed rule set compiles the same rule once per pipeline-set), ormissingwhen the rule set changed while the incident was open. Risk entities join on the entity type as well as its value, and report whether the join came from a retained result'srisk.objectsenrichment or from the incident's own grouping key. - New
GET /api/v1/incidents/{id}serves a single incident. Both per-incident routes return503when the alert pipeline has nogroup:stage, distinguishing "this daemon does not track incidents" from a404for an unknown or already-evicted id. The bundle route additionally returns409while the incident is insidegroup_waitand its contents can still change. - The bundle route requires a new
incident-bundles:readpermission rather than theincidents:readthat gates the incident list, so a custom role can hand out the incident list without also handing out rule documentation and risk entities. The built-inreaderrole (*:read) covers both. - Incident snapshots gain
sample_modeandbundle_ready.sample_modereports which sample kinds an incident actually retained (refs,results,mixed,none), so changingincludewhile an incident is open is visible instead of silently hiding one kind. Both fields are snapshot-only and never appear on emitted incidents. CompiledRuleandCompiledCorrelationretaindescriptionandfalsepositives, andrsigma_evalgains a publicrule_metadatalookup (RuleBundleMetadata,RuleIdentity,RuleMetadataLookup) onEngine,CorrelationEngine,SchemaRouter, andRuntimeEngine. The ADS section vocabulary is now expressed through anAdsCarrierstrait so a compiled rule produces the same nine-section document as a parsed one.
Support list values in add_condition pipeline transformation (#399)
add_conditionaccepts YAML sequences for field values (conditions: {EventID: [12, 13, 14]}), and the values of one field are OR-linked, matching pySigma'sAddConditionTransformation. Pipelines no longer need one transformation per value.- An empty sequence (
field: []) and a non-scalar value (a mapping, or a sequence nested inside a sequence) are load-time errors naming the offending field. Both previously produced a condition that silently matched nothing or disappeared, which widened the rule instead of failing. - Breaking:
AddCondition.conditionschanged fromHashMap<String, SigmaValue>toHashMap<String, Vec<SigmaValue>>. Downstream code constructing theTransformationenum (re-exported asrsigma_eval::pipeline::Transformation) must wrap scalar values in aVec. - The builtin
sysmonpipeline gains the Sigma taxonomy categories it was missing:sysmon_status(EventID 4, 16),registry_event(12, 13, 14),wmi_event(19, 20, 21),file_block_shredding(28), andsysmon_error(255).pipe_creatednow also covers EventID 18. - Behavior change: rules carrying one of those five categories previously got no
EventIDcondition at all and matched every Sysmon event; they now match only the relevant EventIDs. Rules withcategory: pipe_createdalso match EventID 18.
OCSF Detection Finding output (#397)
- Any line-oriented sink can emit OCSF Detection Finding (class 2004) JSON instead of native NDJSON:
--output 'file:///findings.ocsf.ndjson?format=ocsf', or the same suffix on adaemon.output.sinksentry. Findings then flow natively into Splunk, Elastic, CrowdStrike NG-SIEM, and anything else that consumes OCSF. - All four emitted shapes map to class 2004: detections and correlations as
Create, alert-pipeline incidents asCreate/Update/Closeby trigger, and risk incidents asCreate. ATT&CK tags becomefinding_info.attacks[], the rule becomesfinding_info.analytic, the level becomesseverity_id, and the risk layer'srisk.scoreandrisk.objectsenrichments becomerisk_score,resources[], andactor.user.name. - The mapping is lossless: anything without a faithful OCSF home rides under
unmappedwith its native key. Detections stamptimewith the serialization clock because anEvaluationResultcarries no event timestamp; the incident shapes carry real window bounds. Per-result OCSF identifiers and timestamps remain stable across fan-out and delivery retries. Dedup summary records and compact risk events stay native NDJSON sidecar lines, documented as such. - Output only. rsigma does not deserialize OCSF and its input handling is unchanged. The schema version is pinned to
1.1.0, recorded inmetadata.version, and validated by a committed class-2004 conformance fixture (no network in CI). - New public
rsigma_runtime::ocsfmodule with an injectable clock and uid source, plus a new OCSF Findings guide.
Per-sink output format seam (#396)
- Line-oriented sinks (stdout, file, NATS, unix socket) carry a
SinkFormatand serialize through it, so a sink's wire format is a property of the sink rather than hard-coded NDJSON.ndjsonremains the default and its bytes are unchanged. - Sink specs accept a
formatquery parameter (file:///findings.ndjson?format=ndjson) on findings sinks only.formaton an OTLP spec, a--dlqspec, or the audit sink is a startup config error, as is an unknown value; webhooks are declared in YAML files and have no query surface. FormattedIncidentEnvelopewraps the source-compatible nativeIncidentEnvelopewith one pre-serialized line per configured format, so incidents are serialized once per format and each sink delivers its own without breaking existing runtime callers.
Fix fuzz CI rustc ICE on floating nightly (#395)
- Pin the fuzz workflow to
nightly-2026-07-23so AddressSanitizer builds no longer hit the rustc ICE compilingtokiothat started onnightly-2026-07-24(rust-lang/rust#159815).
CI: harden coverage against corrupt LLVM profraw files (#393)
- Daemon integration tests now prefer SIGINT (with SIGKILL fallback) when tearing down instrumented
rsigmachildren so LLVM coverage counters can flush. - The Coverage job scrubs unreadable
.profrawfiles beforellvm-profdata merge, preventing intermittentfile header is corrupt/no profile can be mergedfailures.
Fix scheduled fuzz failures (#394)
- rstix pattern lexer:
decode_hexwalks ASCII nibbles by byte instead of slicing the&strat fixed offsets, so multi-byte UTF-8 in anh'…'literal returns a lex error instead of panicking on a mid-character boundary. - fuzz_template_expand: drop removed
Pipeline::sourcesfield after source declarations moved out of pipeline structs. - fuzz CI: create missing
seeds/<target>directories beforecargo fuzz run, and seedfuzz_rstix_parse_bundleso the weekly job no longer exits on a missing seeds path.
rstix: STIX 2.1 wire conformance closure (#388)
- T0 MUST enforcement — non-empty SDO
nameand groupingcontext; non-empty report, grouping, note, and opinionobject_refs;malware-analysistime ordering; IPv4/IPv6/MAC address format; RFC 3986 URL validation; hash map keys inHASH_ALGORITHM_ENUMorx_extension form with known-algorithm value formats; artifactencryption_algorithmclosed vocabulary aligned with STIX 2.1 §10.4. - Open vocabulary (
*-ov) at parse —grouping.context,malware-analysis.result, andwindows-pebinary-ext.pe_typeaccept any string at parse (STIX §2.14); suggested-vocabulary membership is reported asSTIX-I0001(Info) and does not failValidator::interop_strict(). object_refs/language-contentrefs — bundle ref kind checks accept any STIX Object (SDO, SCO, SRO, Meta), not only SDO/SCO.- Open vocabulary tables —
vocab/open.rsregenerated from normative STIX 2.1 §10 value tables. - Validation Pipeline —
STIX-I0001open-vocabulary extensions default to Info severity (spec-legal custom values passinterop_strict);STIX-I0002relationship matrix remains Warning (failsinterop_strict). - Tests — negative fixtures wired in
tests/spec.rs; encryption moved from T1 warning to T0 parse error; pattern eval fixtures updated for valid hash values. - §7.1.1 language-content — keys for properties that do not exist on the target object are silently ignored at parse and validation (removed over-strict
LanguageContentFieldUnknownrejection and advisory warnings);object_modifiedmismatch and translation mirroring for existing target fields remain MUST at parse. - Unmodeled top-level properties — restore bundle capture in
common.extra/Bundle::extra_properties()instead of rejecting non-x_keys at parse (removedUnknownTopLevelProperty). - §7.2.3 granular selectors — selectors MUST resolve on object wire JSON at parse (unchanged).
- Docs — README, library docs, plan audit files, and cursor rules aligned with MUST vs SHOULD vs ignore tiers.
Documentation site analytics and privacy controls (#392)
- The documentation site uses GA4 with explicit cookie consent, Google Consent Mode defaults that keep analytics storage disabled until acceptance, permanently disabled advertising consent, and a dedicated privacy notice.
CLI: complete --output-format coverage (#389)
- Structured report commands (
backend targets/formats,rule validate,pipeline resolve/diff,config validate/show/path, and the existing eval/lint/fields/report family) honorjson/ndjson/table/csv/tsvthrough a shared renderer backed by thecsvcrate. backend convertemits one NDJSON query record per line for--output-format ndjson; table/csv/tsv warn and keep raw query text.- AST commands (
rule parse/condition/stdin) warn and fall back to JSON for table/csv/tsv instead of silently ignoring the selector. - Fixed products (
rule reverse,rule draftYAML,rule migrate-sources,engine tap,engine daemon,mcp serve,config init/reload) keep their wire format and emit a standardized unsupported-format warning when an incompatible selector is set. config validate/showlocal--formatwins over global--output-formatwith a precedence warning when both are set.- CI runs
scripts/output-format-smoke.shagainst a full-featured binary to keep the per-command contract from drifting.
rstix: TAXII collection ingest (taxii-store feature) (#387)
taxii-store— meta-feature (taxii+store).ingest_collection/ingest_collection_with_bundle_id— paginated fetch with per-pageStixStore::import_objects(bounded memory); mergedImportReportwith final ref audit against the store.IngestError— wrapsTaxiiErrorandStoreError.- TAXII capability checks — with
CapabilityPolicy::Enforce, collectionmedia_typesmay includeapplication/taxii+jsonor STIX 2.1 types; absentmedia_typesdefaults toapplication/stix+json(TAXII §5.2.1); API Rootmax_content_lengthmust be a JSON integer > 0 (strict parse, no string coercion). StixStore::import_bundle— unresolved refs are reported only when the target id is missing from both the imported bundle and the store (incremental TAXII ingest).
rstix: TAXII DANE DNSSEC validation (taxii feature) (#381)
TaxiiClientConfig::dane_require_dnssec— defaulttruewhenServerTrustPolicy::Dane; DNSSEC-validates TLSA prefetch and SRV discovery (TAXII §8.5.2 SHOULD). Setfalsefor unsigned lab DNS only.DnsLookupOptions—validate_dnsseconresolve_*_with_optionshelpers (defaultfalsefor standalone calls).