Skip to content

v0.21.0

Latest

Choose a tag to compare

@mostafa mostafa released this 05 Aug 14:26
56f5f88

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).
  • rstix STIX 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-valued add_condition (#399, thanks to @frack113).
  • Operator surfaces: per-sink output formats (#396), complete --output-format coverage (#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-gating examples/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, Consumer REQ-3.2-C-01..C-05, checklist REQ-CHK-SXP-3.2 / REQ-CHK-SXC-3.2, examples REQ-3.2-EX-4.1 / EX-4.2.
  • Table 4 CSD01 typo (type = threat-actor) is documented; tests enforce STIX §4.2 campaign.
  • Tightens the equivalent §3.1 Attack Pattern rows in the same pass: P-02 pins that caller selection renames exactly one object and that the selected name survives parse and re-validation, and P-07 reads 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 in tests/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-gating examples/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, Consumer REQ-3.1-C-01..C-05, checklist REQ-CHK-SXC-3.1 / REQ-CHK-SXP-3.1, examples REQ-3.1-EX-4.1 / EX-4.2 / EX-7.1.
  • ex-3.1.4.2 retains published truncated UUID ids (defect 21) and asserts parse rejection — no invented digits.
  • Each requirement row carries evidence distinct from its siblings: P-04 runs AttackPattern::validate plus a strict bundle report instead of repeating the P-01 gate, C-04 reads fields through QueryableStixObject and validates typed leaves, and REQ-CHK-SXC-3.1 closes created_by_ref to a typed Identity rather than re-validating producer data. P-05 asserts the wire type, P-11 and P-12 require exactly three subsecond digits, and EX-4.2 matches ParseError::InvalidStixId(InvalidUuid) instead of substring-matching the error text.
  • Adds ParseError::InvalidStixId so truncated or otherwise invalid STIX ids survive the serde boundary as typed errors (same tagging approach as ModelError).
  • 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 as BLOCKED on 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() and exempt_predefined_tlp_marking_refs extend 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.name is now Option<String> at parse, matching STIX §4.4 (only context and object_refs are required on grouping). Callers that constructed or read Grouping.name as String must handle Option / provide Some(...) 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_index module and its RuleIndex type are gone, replaced by an internal CandidateIndex. The index was never usable on its own (it indexed CompiledRule slices the engine owned), so callers should reach for Engine instead.
  • 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_total and the daemon's rsigma_rules_pruned_by_logsource_total counter 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 filter whose 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 common selection and not filter shape, which stays reachable through its positive conjunct; the witness-based index restores pruning for it.
  • --cross-rule-ac indexed |cased needles 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-ac fed 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-prefilter inserted no bits for needles shorter than the three-byte trigram window, so a probe could answer DefinitelyNoMatch for 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 single message field with product/service hint fields) and sysmon_file_event (Sysmon EventID 11 FileCreate with product/category hints and a string EventID, as some forwarders emit it).
  • The new lanes show that --logsource-routing and --cross-rule-ac do 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.md and the performance tuning guide carry the measured rows. Witness-based candidate indexing (below) supersedes the guidance these lanes produced: --logsource-routing alone 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.rs silent-skip guard, tests/fixtures/interop/manifest.toml, provenance-aware fixture loader, overlay on Validator::interop_strict() (SHOULD-level STIX-I0002 downgrade), bundle-closure checker with TLP exemption, containment helpers, linkme self-registration inventory, and certification report generation to target/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.sh pins the SigmaHQ corpus to the CI SHA and scripts/perf/gen_events.py deterministically generates six event lanes (raw Windows blobs, structured Windows, mixed schema, no-match, low-match, match-heavy). scripts/perf/baseline-eval.sh runs the offline eval flag/thread matrix and scripts/perf/baseline-daemon.sh measures daemon HTTP end-to-end throughput from the daemon's own processed-events counter.
  • New witness_audit example in rsigma-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.md now 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.wait previously selected the default group_wait: 30s without 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 by GET /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, ambiguous when several loaded rules carry it with differing documentation (a routed rule set compiles the same rule once per pipeline-set), or missing when 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's risk.objects enrichment or from the incident's own grouping key.
  • New GET /api/v1/incidents/{id} serves a single incident. Both per-incident routes return 503 when the alert pipeline has no group: stage, distinguishing "this daemon does not track incidents" from a 404 for an unknown or already-evicted id. The bundle route additionally returns 409 while the incident is inside group_wait and its contents can still change.
  • The bundle route requires a new incident-bundles:read permission rather than the incidents:read that 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-in reader role (*:read) covers both.
  • Incident snapshots gain sample_mode and bundle_ready. sample_mode reports which sample kinds an incident actually retained (refs, results, mixed, none), so changing include while an incident is open is visible instead of silently hiding one kind. Both fields are snapshot-only and never appear on emitted incidents.
  • CompiledRule and CompiledCorrelation retain description and falsepositives, and rsigma_eval gains a public rule_metadata lookup (RuleBundleMetadata, RuleIdentity, RuleMetadataLookup) on Engine, CorrelationEngine, SchemaRouter, and RuntimeEngine. The ADS section vocabulary is now expressed through an AdsCarriers trait so a compiled rule produces the same nine-section document as a parsed one.

Support list values in add_condition pipeline transformation (#399)

  • add_condition accepts YAML sequences for field values (conditions: {EventID: [12, 13, 14]}), and the values of one field are OR-linked, matching pySigma's AddConditionTransformation. 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.conditions changed from HashMap<String, SigmaValue> to HashMap<String, Vec<SigmaValue>>. Downstream code constructing the Transformation enum (re-exported as rsigma_eval::pipeline::Transformation) must wrap scalar values in a Vec.
  • The builtin sysmon pipeline 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), and sysmon_error (255). pipe_created now also covers EventID 18.
  • Behavior change: rules carrying one of those five categories previously got no EventID condition at all and matched every Sysmon event; they now match only the relevant EventIDs. Rules with category: pipe_created also 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 a daemon.output.sinks entry. 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 as Create/Update/Close by trigger, and risk incidents as Create. ATT&CK tags become finding_info.attacks[], the rule becomes finding_info.analytic, the level becomes severity_id, and the risk layer's risk.score and risk.objects enrichments become risk_score, resources[], and actor.user.name.
  • The mapping is lossless: anything without a faithful OCSF home rides under unmapped with its native key. Detections stamp time with the serialization clock because an EvaluationResult carries 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 in metadata.version, and validated by a committed class-2004 conformance fixture (no network in CI).
  • New public rsigma_runtime::ocsf module 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 SinkFormat and serialize through it, so a sink's wire format is a property of the sink rather than hard-coded NDJSON. ndjson remains the default and its bytes are unchanged.
  • Sink specs accept a format query parameter (file:///findings.ndjson?format=ndjson) on findings sinks only. format on an OTLP spec, a --dlq spec, 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.
  • FormattedIncidentEnvelope wraps the source-compatible native IncidentEnvelope with 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-23 so AddressSanitizer builds no longer hit the rustc ICE compiling tokio that started on nightly-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 rsigma children so LLVM coverage counters can flush.
  • The Coverage job scrubs unreadable .profraw files before llvm-profdata merge, preventing intermittent file header is corrupt / no profile can be merged failures.

Fix scheduled fuzz failures (#394)

  • rstix pattern lexer: decode_hex walks ASCII nibbles by byte instead of slicing the &str at fixed offsets, so multi-byte UTF-8 in an h'…' literal returns a lex error instead of panicking on a mid-character boundary.
  • fuzz_template_expand: drop removed Pipeline::sources field after source declarations moved out of pipeline structs.
  • fuzz CI: create missing seeds/<target> directories before cargo fuzz run, and seed fuzz_rstix_parse_bundle so 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 name and grouping context; non-empty report, grouping, note, and opinion object_refs; malware-analysis time ordering; IPv4/IPv6/MAC address format; RFC 3986 URL validation; hash map keys in HASH_ALGORITHM_ENUM or x_ extension form with known-algorithm value formats; artifact encryption_algorithm closed vocabulary aligned with STIX 2.1 §10.4.
  • Open vocabulary (*-ov) at parsegrouping.context, malware-analysis.result, and windows-pebinary-ext.pe_type accept any string at parse (STIX §2.14); suggested-vocabulary membership is reported as STIX-I0001 (Info) and does not fail Validator::interop_strict().
  • object_refs / language-content refs — bundle ref kind checks accept any STIX Object (SDO, SCO, SRO, Meta), not only SDO/SCO.
  • Open vocabulary tablesvocab/open.rs regenerated from normative STIX 2.1 §10 value tables.
  • Validation PipelineSTIX-I0001 open-vocabulary extensions default to Info severity (spec-legal custom values pass interop_strict); STIX-I0002 relationship matrix remains Warning (fails interop_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 LanguageContentFieldUnknown rejection and advisory warnings); object_modified mismatch 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 (removed UnknownTopLevelProperty).
  • §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) honor json/ndjson/table/csv/tsv through a shared renderer backed by the csv crate.
  • backend convert emits 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 draft YAML, 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/show local --format wins over global --output-format with a precedence warning when both are set.
  • CI runs scripts/output-format-smoke.sh against 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-page StixStore::import_objects (bounded memory); merged ImportReport with final ref audit against the store.
  • IngestError — wraps TaxiiError and StoreError.
  • TAXII capability checks — with CapabilityPolicy::Enforce, collection media_types may include application/taxii+json or STIX 2.1 types; absent media_types defaults to application/stix+json (TAXII §5.2.1); API Root max_content_length must 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 — default true when ServerTrustPolicy::Dane; DNSSEC-validates TLSA prefetch and SRV discovery (TAXII §8.5.2 SHOULD). Set false for unsigned lab DNS only.
  • DnsLookupOptionsvalidate_dnssec on resolve_*_with_options helpers (default false for standalone calls).

v0.20.0...v0.21.0