Skip to content

v0.24.0

Latest

Choose a tag to compare

@mostafa mostafa released this 05 Oct 10:19
· 3 commits to main since this release
v0.24.0
c669c7a

TL;DR
RSigma v0.24.0 is the "conformance, reliability, and stability" release: Sigma rules now fail early when their semantics are invalid; modifiers, keywords, selectors, filters, correlations, and processing pipelines align more closely with Sigma and pySigma; and native backend queries are verified against real PostgreSQL, LynxDB, and Fibratus engines.

  • Conformance: parse-time semantic validation rejects unusable logsources, invalid modifier/value combinations, malformed regexes and CIDRs, empty detections, and unresolved condition selectors before compilation (#543).
  • Sigma semantics: value modifiers, keywords, selectors, filters, temporal and chained correlations, and processing pipelines now follow the Sigma specification and pySigma more closely (#524, #531, #532, #534, #537, #539, #540, #541).
  • Conversion and reliability: native backends preserve condition precedence, encoding variants, regex flags, typed values, negation, filters, and ordered temporal correlations, with regression tests in PostgreSQL, LynxDB, Fibratus, and pySigma (#528, #530, #542).
  • Breaking: parser errors move earlier, HIR_SCHEMA_VERSION is 3, processing-pipeline and correlation APIs change, and the PostgreSQL case_sensitive_re option is removed. See the migration notes in the entries below.
  • Platform and docs: Rust 1.96.0 is pinned across development and release builds; deployment, tutorial, troubleshooting, version-tag, architecture, and visual documentation are refreshed; CI, release publishing, dependency, performance, and container scanning workflows are hardened (#516 through #521, #533, #538, #544, #545, #550).

Rust toolchain pinned to 1.96.0 (#545)

The workspace MSRV rises from Rust 1.95.0 to 1.96.0. Local development, CI, release binaries, crates.io publishing, performance runs, backend engine tests, and the Docker builder now use the exact compiler pinned in rust-toolchain.toml instead of floating stable; fuzzing remains on its separately pinned nightly. A shared workflow action installs only each job's required components and targets, and CI rejects version drift across Cargo metadata, the toolchain file, Docker, the README badge, and the contributing guide.

Dependency batch (Oct 2026) (#544, #550)

Rolls up the compatible open Dependabot PRs into a single merge, with Cargo.lock regenerated against current main. Rust: the patch group (#526) updates pest/pest_derive 2.9.1 to 2.9.2, thiserror 2.0.20 to 2.0.21, rand 0.10.2 to 0.10.3, evtx 0.12.2 to 0.12.3, clap 4.6.6 to 4.6.7, hyper-util 0.1.20 to 0.1.21, and encoding_rs 0.8.41 to 0.8.42; tokio-postgres-rustls moves from 0.13.0 to 0.14.0 (#527). CI (all repinned by commit SHA, #525): taiki-e/install-action v2.87.19 to v2.87.20 and github/codeql-action/upload-sarif v4.38.1 to v4.38.2. The VS Code extension updates vscode-languageclient 10.1.1 to 10.1.2 (#529). A follow-up updates async-nats 0.49.1 to 0.50.0 (#549), opentelemetry-proto 0.32.0 to 0.33.0 (#548), taiki-e/install-action v2.87.20 to v2.87.21 (#547), and the VS Code extension's @types/node 26.6.2 to 26.6.3 (#546). Held back: yamlpath 1.30.1 (#495) and yamlpatch 1.30.1 (#494) require rustc 1.97 through tree-sitter-iter, above the 1.96.0 MSRV; tikv-jemallocator 0.7.0 (#425) remains excluded after the measured musl daemon throughput regression.

The parser validates rule semantics before compilation (#543)

The parser now validates each rule's meaning as well as its structure, moving pySigma-aligned checks earlier and requiring a usable logsource, so rule parse, rule validate, engine eval, the daemon, conversion, the LSP, and the MCP tools report a broken rule as a parse error that names the field instead of a compile error, or instead of accepting it. The new rsigma_parser::validate module holds the checks. A rule now fails to parse when:

  • its logsource is missing, or sets none of category, product, and service, or one of those keys or definition is not a string. Filter rules need a logsource as well, as the Sigma filter specification requires.
  • a field combines conflicting modifiers, such as two operators (|gt|lt), two UTF-16 encodings, base64 with base64offset, or a string modifier before fieldref.
  • a value has the wrong type for its modifiers: a number or boolean under contains, startswith, endswith, re, cidr, an encoding, fieldref, expand, or cased, or anything but a YAML number under gt, gte, lt, lte, or a timestamp part such as minute.
  • a re value is not a valid regular expression, a cidr value is not address/prefix or has host bits set, a fieldref, base64, or base64offset value contains a wildcard, or a UTF-16 encoding without base64 or base64offset has a non-ASCII value.
  • exists is applied to a keyword or takes anything but a single YAML boolean, true or false. Previously any other value, such as 'yes' or maybe, was treated as true.
  • |all has no values, an empty value list is not bound to a field, a field value is a nested list or mapping, or a detection list contains a list.
  • a named detection or condition list is empty, a keyword list contains null, or the condition references a detection identifier that does not exist or a 1 of x* selector that matches none.

A regular expression with lookaround or backreferences, which pySigma accepts, still parses, and the evaluator rejects it when the rule compiles, because its regex engine does not support those constructs. Lowering repeats the detection-item checks as a backstop for items code or a pipeline rewrites after parsing, while invalid regex syntax still reaches the evaluator's regex compiler and its specific EvalError::InvalidRegex error. rule reverse exits with code 3 unless --logsource-product, --logsource-category, or --logsource-service is set, since the rule it would write no longer parses.

The new deprecated_detection_timeframe lint warns about a Sigma v1.x timeframe: key inside detection:, which has no effect. rule validate now accepts a single rule file as well as a directory.

Migration notes:

  • Add a logsource with at least one of category, product, or service to every detection and filter rule. A filter applies only to rules whose logsource includes every key the filter sets, so give a filter only the keys its rules share, such as product: windows.
  • Fix or remove values that the new checks reject. Run rsigma rule validate on the rules directory to list them; every message names the field and the problem.
  • Pass a --logsource-* flag to rule reverse.

Breaking changes for library users: parse_sigma_yaml, parse_sigma_file, and parse_sigma_directory report the rules above as parse errors, so code that built rules without a logsource or with invalid values must fix them. SigmaParserError has a new InvalidModifiers variant. Lowering a detection item that applies exists to a keyword or to a non-boolean value fails with IrError::IncompatibleValue, and rsigma_convert's reverse conversion fails with ConvertError::RuleConversion when no logsource is configured.

Converted queries match what the engine evaluates (#542)

Every native backend now converts the encoding modifiers (windash, wide, utf16le, utf16be, utf16, base64, base64offset) into an OR of one plain match per encoded variant, as pySigma does, instead of failing with UnsupportedModifier. The OR stays grouped under an enclosing AND, and eval and conversion share the expansion through the new rsigma_ir::encoding module, so both agree on the variants a rule matches. A UTF-16 encoding without a following base64 or base64offset produces NUL characters, which the PostgreSQL and Fibratus backends reject with UnsupportedValue. An expand value with placeholders no pipeline resolved now names the placeholders in its conversion error.

A cidr value with host bits set (10.1.2.3/8), or one that is not address/prefix, is now rejected when the rule is lowered, so evaluation and every backend refuse it as pySigma does instead of converting it. Library users of rsigma-eval see EvalError::IncompatibleValue for such values instead of EvalError::InvalidCidr. The test backend rejects |fieldref|cased with UnsupportedModifier instead of dropping cased.

Regex flags now reach every backend. A plain re converts to PostgreSQL's case-sensitive ~ instead of ~*, as the Sigma specification requires, and |i selects ~*. |m adds PostgreSQL's (?w) embedded option, joining a leading (?i) group because PostgreSQL reads only one, so ^ and $ match at line breaks. The test, LynxDB, and Fibratus backends prepend the i, m, and s flags as an inline group such as (?i), which they previously dropped, and the parser rejects re|cased as pySigma does. The new RegexFlags::inline_prefix renders the group for custom backends.

PostgreSQL plain equality is now case-insensitive, as Sigma requires: User: admin renders as "User" ILIKE 'admin' with %, _, and \ escaped, and only |cased keeps =. Previously = missed ADMIN. A value without letters, such as EventID: '4624', still renders as =, which matches the same rows and works on a non-text column; a value with letters needs a text column.

PostgreSQL renders negation as (expr) IS NOT TRUE instead of NOT expr. A comparison on a missing field is NULL, and NOT NULL dropped the row, so selection and not filter and |neq missed events that lack the filtered field. The [all] and [all_or_empty] array blocks use the same form, so an element without the tested field now fails the block instead of passing it.

PostgreSQL JSONB mode compares typed values correctly. A number or a lt/lte/gt/gte comparison casts the ->> text to numeric when it reads as a number, instead of comparing text with an integer, which PostgreSQL rejected with "operator does not exist: text = integer". The digit counts are bounded, so an event with an oversized value such as 9e999999 compares as NULL instead of failing the query with a numeric overflow. A boolean compares the text with ILIKE 'true' instead of failing on text = boolean. Elements of a scalar array such as ports[any]: 4444 get the same treatment. exists tests key presence through ->, so a field whose value is JSON null now exists.

PostgreSQL temporal_ordered correlations now enforce the order of rules, which they previously ignored, rendering the same query as temporal. One CTE per referenced rule takes the earliest hit of that rule at or after the previous rule's hit, per group, and the query keeps a group only when the chain completes, in the sliding, tumbling, and session window modes and within each hourly bucket of the timescaledb format. Single-table temporal and temporal_ordered queries also select * instead of *, rule_name, which duplicated the column and made PostgreSQL reject the query as ambiguous, and multi-table queries escape quotes in rule names, which they inserted into the SQL verbatim.

LynxDB keeps a native search only when every value in a rule condition is one LynxDB's search matches exactly, and otherwise renders the whole condition as FROM <index> | where .... Regexes and CIDR were appended as global | where stages, which applied them to the whole query even when they sat under an OR or NOT, and the =~ operator matched nothing once events were flushed to segments; they now render as match() and cidrmatch() in place. cased keeps contains, startswith, and endswith instead of turning into whole-value equality, ? matches one character instead of any run, a literal * no longer acts as a wildcard, and values with /, quotes, >, or a * between literals match the literal text. In search, a value with outer wildcards keeps its literal quoted, so a space or backslash in it no longer splits or doubles it, and in where a keyword wildcard stays within one JSON string of the raw event. Numeric comparisons parse numeric strings and skip other values, null no longer misses a field present with a null value, and an empty string matches through the event's raw JSON, because LynxDB columns store it as null. The minimal format renders a where query as * | where ..., even when a value contains | search.

Fibratus exists: true renders as field != '' instead of field != false, which never matched a string field, and exists: false as field = '' instead of field = false. Fibratus reads a field the event lacks as its type's zero value, so an empty-string comparison is the only absence test it has, and it also treats a present empty string as absent. The Fibratus engine tests now fill absent fields the same way. A contains, startswith, or endswith value with a wildcard becomes a glob with the operator's own leading or trailing *, since the substring operators read * as a literal character. A literal * or ? stays verbatim instead of being backslash-escaped, which the Fibratus parser rejected as a bad escape, and a glob that also has one renders as an anchored regex(). A line break in a value is written as \n, since the parser rejects a raw one.

The test backend now renders what pySigma's TextQueryTestBackend renders. Backslashes in values are no longer doubled, a literal * or ? keeps its escape, and every string value is quoted. The expression follows the value's wildcard shape, so endswith: 'sys*.exe' renders as match "*sys*.exe" and a value of *whoami* as contains "whoami". A value list, the items of one selection, or same-field selections joined by a condition render as Field in (...) for OR and Field contains-all (...) for AND, while encoding variants stay an OR. CIDR matches render as cidrmatch('Field', "cidr"), and backslashes in regexes are escaped. cased values stay out of in-lists, which pySigma renders case-insensitively. The new rsigma_ir::encoding::expand_encoded_matcher expands the encodings of a single matcher.

Conversion now applies Sigma filters, as pySigma does when a collection loads: convert_collection, and so backend convert, hunt, and the MCP convert_rules tool, merges each filter into the detection rules it references before pipelines run, so field mappings reach the filter's fields. The new rsigma_eval::apply_filters performs the merge and targets rules the same way Engine::apply_filter does. A correlation rule converted with a backend that has no correlation support (lynxdb and test) is now an UnsupportedCorrelation error instead of being dropped silently; pass --skip-unsupported to convert the rest.

Breaking changes for library users: the PostgresBackend::case_sensitive_re field is removed, and an invalid cidr value fails with EvalError::IncompatibleValue instead of EvalError::InvalidCidr.

Migration notes:

  • Add |i to a re value that should keep matching case-insensitively in PostgreSQL. The case_sensitive_re PostgreSQL option is removed because plain re is now case-sensitive, and -O case_sensitive_re=... is ignored like any unknown option.
  • LynxDB queries for rules with regexes, CIDR, null, cased, numeric comparisons, or values search cannot match exactly now start with FROM <index> | where instead of FROM <index> | search, and scan every event in the index. Saved queries that parse the search expression need to accept both forms.

Chained correlations follow Sigma output semantics, and corpus replay counts referenced rules (#541)

In a chain where a detection feeds a correlation that feeds another correlation, engine eval, the daemon, the MCP evaluate_events tool, and backend convert now output only the top correlation, as pySigma does, unless a referencing correlation has top-level generate: true. A PostgreSQL aggregate correlation (event_count, value_count, and the value_* types) that references another correlation, or a detection rule that failed to convert, now fails to convert instead of counting every row in the table. The referenced correlation still updates its parent's state, and --emit-detections and emit_detections now emit referenced correlations as well as referenced detections. Library users of CorrelationEngine see the same change, and emit_detections: true restores the previous output. rule backtest and rule hygiene count fires of rules that a correlation references again, so those rules are no longer reported silent. The deprecated daemon.correlation.no_detections config key takes effect again and prints a deprecation warning. CorrelationEngine::process_batch keeps the rule identity of events without a timestamp, so name-only rules that share a title still feed their correlations. rule validate reports a correlation reference that resolves to more than one rule, through a duplicate id or name or a name that equals another rule's id.

Filter selectors skip hidden items and generated filters target rule names (#540)

A selector with a * between two literal parts no longer matches a name where the parts overlap, so sel*lection no longer selects selection in a detection condition, and not 1 of *_main in a filter no longer selects a filter item named main. In a filter condition, them and patterns that do not start with _ now skip the filter's _-prefixed detection items, as they already did in detection conditions. rule tune and the MCP tune_rules tool now target a rule by name when it has no id, instead of writing the title reference that rule lint reports as deprecated. rule lint resolves filter references against detection rules only, matching the engine, so a correlation name no longer hides a filter's title reference.

Processing pipelines follow the pySigma condition and transformation dialect (#539)

Processing pipelines now apply pySigma condition linking consistently at rule, detection-item, and field-name scope. The canonical *_cond_op, *_cond_not, and *_cond_expr keys are supported, rule_cond_expression remains an alias, condition collections accept lists or identifier-keyed mappings, and list identifiers are one-based. Unknown transformation-item keys, invalid operators, and unresolved expression references now fail pipeline parsing instead of being ignored.

set_state reads typed values from val while retaining value as an alias, and processing_state supports eq, ne, gte, gt, lte, and lt, with eq and ne comparing numbers by value. Field-name transformations honor detection-item conditions and also rewrite fieldref targets and the rule's fields list. Placeholder transformations now operate only on |expand values, support mutually exclusive include/exclude filters, expand the Cartesian product of multiple variables, and report unresolved value_placeholders variables unless allow_unresolved: true explicitly enables runtime substitution; wildcard_placeholders replaces every handled placeholder with *. A backslash escapes % in placeholder values as it does in the expand modifier.

Conditions match the way pySigma evaluates them:

  • match_string and field-name regexes match from the start of the text. match_string sees the value with contains, startswith, and endswith wildcards added and literal wildcards escaped, never matches field references or other non-string values, applies negate per value, and accepts cond: any|all. is_null accepts cond too.
  • Field-name conditions select a detection item through its field name or any of its field reference targets, for every transformation, including drop_detection_item and detection_item_failure.
  • include_fields and exclude_fields read pySigma's mode: plain|re key, with match_type kept as an alias.
  • processing_item_applied tracks renamed fields at field-name scope and changed items at detection-item scope instead of reporting whether the transformation ran anywhere in the rule.
  • nest applies each inner item under its own conditions only, and correlation rules honor field-name conditions, nest, and field_name_transform.

Transformation parameters from pySigma are implemented or rejected instead of being ignored. add_condition accepts name and template, replace_string accepts interpret_special, and set_value accepts force_type. hashes_fields now replaces each Hashes or Hash item (configurable with field_to_parse) with an OR over per-algorithm fields, accepts ALGO|hash and bare hashes whose algorithm comes from their length, requires valid_hash_algos, and fails when no valid algorithm is found. regex validates method and rejects plain, field_name_transform rejects apply_keyword: true, and query_expression_placeholders rejects mapping, include, and exclude.

Migration notes:

  • Add |expand to values whose placeholders a pipeline should expand; plain values and keywords are no longer expanded.
  • Set allow_unresolved: true on value_placeholders to keep runtime substitution for variables the pipeline does not define.
  • hashes_fields no longer prefixes field names with File by default and no longer splits comma-separated values. Set field_prefix: File to keep the previous field names.
  • Add cond: all where a match_string or is_null condition must hold for every value.

Breaking changes for library users:

  • TransformationItem stores each condition scope in ConditionSet<T> instead of separate vectors and flags. Access parsed conditions through .conditions and use one-based string identifiers for positional conditions. The legacy NamedRuleCondition type and all_rule_conditions_match helper remain available for callers that only need AND linking.
  • Each ProcessingState condition carries a typed val and a StateOperator; set StateOperator::Eq for the previous equality behavior. Transformation::SetState.value is a serde_json::Value; wrap strings with serde_json::Value::String.
  • DetectionItemCondition::MatchString and IsNull carry a cond: ValueMatch; ValueMatch::Any keeps the previous linking.
  • Transformation::AddCondition gains name and template, ReplaceString gains interpret_special, HashesFields gains field_to_parse, and the placeholder variants carry their new options. Use None, false, and vec!["Hashes".into(), "Hash".into()] for the previous behavior.
  • PipelineState replaces the public detection_item_applied set, reset_detection_item(), and was_applied_to_detection_item() with detection_item_was_processed_by(), and adds field_name_applied, track_field_rename(), and field_was_processed_by().
  • eval_condition_expr returns false for an expression that does not parse, such as an empty string.
  • Transformation::apply with an empty field-name condition list and field_name_cond_not: true no longer renames fields, because the negated empty set is false.

Grype scans keep GitHub code scanning current (#538)

The Docker workflow now scans both architectures of the published image weekly and uploads the results against the default branch. Scheduled scans reuse the existing Grype configuration without rebuilding or republishing the image.

Filter selectors and rule references follow Sigma semantics (#537)

Filter condition selectors now remain scoped to the filter's own detection items after those items are namespaced and injected into a target rule. Patterns such as not 1 of selection_* and all of them no longer resolve against similarly named detection items from the target rule. filter.rules now resolves rule name values as well as id. Exact title matching remains available for compatibility and emits the new filter_reference_by_title lint warning so rulesets can migrate to stable identities. A title reference previously reported unknown_rule_reference, so lint suppressions or tooling keyed on that code should move to filter_reference_by_title.

Correlation rules follow Sigma identity, validation, grouping, and output semantics (#534)

Correlation evaluation, conversion, validation, and linting now agree on Sigma correlation semantics:

  • Correlations can reference detection rules and earlier correlations by name even when they have no id, including through schema routing and chained correlations.
  • Events missing any group-by field, or carrying a non-scalar group value, do not enter correlation state.
  • Zero-length timeframes, unknown condition keys, and value aggregations without a condition field are rejected during parsing.
  • rule validate reports correlation references that resolve to no rule id or name.
  • Referenced detection matches are no longer standalone output by default. Top-level generate: true on a correlation emits its referenced detections, while engine eval and engine daemon accept --emit-detections to emit every detection match. The deprecated --no-detections flag remains accepted. Daemon configuration uses correlation.emit_detections, with the inverse no_detections key retained for compatibility. The MCP evaluate_events tool exposes the same opt-in behavior.
  • Correlation-capable conversion omits referenced standalone rules unless at least one referencing correlation has generate: true.
  • rule lint emits the informational correlation_only_references hint when a correlation omits generate: true.

Breaking changes for library users: CompiledRule has a new public name field, so struct literals must set it, and LintRule has a new CorrelationOnlyReferences variant, so exhaustive matches need an arm for it. GroupKey::extract and GroupKey::from_pairs return Option<Self>, which is None when a group-by value is missing or not a scalar. CorrelationConfig::default().emit_detections is now false instead of true; set emit_detections: true to keep the previous output.

Performance regression checks run only for relevant changes (#533)

The 20-minute coarse performance regression gate no longer runs for every change under crates/. Pull requests trigger it only when they change the evaluator hot path, dependencies, the Rust toolchain, or the performance harness. Changes limited to conversion backends, MCP, LSP, STIX, docs, integration tests, benchmarks, or unrelated CLI/daemon code no longer wait for the gate. Weekly and manually dispatched measurements still run the full performance matrix.

Keywords and condition selectors follow the Sigma specification (#532)

engine eval, the daemon, and backend convert now interpret keyword detections and condition selectors the way the Sigma specification and pySigma define them. The new Keywords and Conditions reference describes the semantics.

  • A field-less |all list, such as '|all': ['bash -c', '/dev/tcp/'], never matched, because one event value had to equal every term, and conversion failed for a missing field name. Each value must now occur somewhere in the event, a field-less value without a string operator matches as a substring, and |neq holds when no event value matches. Conversion renders one full-text term per value. A field-less value inside an array body still matches the member itself.
  • A numeric keyword such as 4624 never matched. Keyword numbers now match their decimal text, and the keyword search covers numeric event values.
  • A selector pattern such as 1 of * or all of sel* also matched detections whose names start with an underscore. Only patterns that start with an underscore select them now, as them already did.
  • A selector that matches no detection, such as all of zzz*, was true in eval while conversion rejected the rule. It is now a compile error everywhere (IrError::NoSelectorMatches, which now carries the selector text).
  • The PostgreSQL backend rendered keyword searches over flat columns as ROW(*)::text, which PostgreSQL rejects, so keyword rules failed to run without -O json_field. The row is now referenced by the unqualified table name.

Value modifiers follow the Sigma specification (#531)

engine eval, the daemon, and filters now interpret value modifiers the way the Sigma specification and pySigma define them. Rules and filters share one interpretation, since filters are compiled through the IR like rules. The new Value Modifiers reference describes the semantics.

  • A wildcard under startswith or endswith was anchored at the wrong end, so CommandLine|startswith: 'net*user' matched cmd /c net user and missed net localuser /add. Wildcards now also match newlines.
  • base64offset kept a character that depends on the byte after the value, so Data|base64offset|contains: Test missed the encodings of Testing. It now trims each variant the way pySigma does.
  • windash only treated - as interchangeable, so rules written with / never matched the - form. Every -, /, en dash, em dash, and horizontal bar is now interchangeable with the others, wildcards keep their meaning (dir*-s), and windash combined with base64 encodes every variant instead of being ignored.
  • wide, utf16, and utf16be without base64 matched the plain text. They now match the UTF-16 string, and reject non-ASCII values.
  • A wildcard under base64 or base64offset, which an encoding cannot represent, is rejected instead of being encoded as a literal star.
  • An expand value always compiled to whole-value equality, ignoring contains, startswith, endswith, and wildcards from pipeline variables. A value whose placeholders a pipeline resolved now matches like any other value and converts with backend convert; placeholders left for match time honor the string operator. A backslash now escapes % as the specification defines, so C:\Users\%user% must be written C:\Users\\%user% to keep the placeholder.
  • exists treated a field set to null as absent. It is now a presence check.
  • neq never matched an event without the field, so User|neq: admin skipped events with no User. It now matches missing and null fields, and the PostgreSQL backend no longer adds IS NOT NULL to a negated field reference.
  • An empty value list (Field: []) is now a null check, as in pySigma.
  • A modifier repeated in one key, such as Field|neq|neq, is now a parse error (SigmaParserError::DuplicateModifier).

Breaking changes for library users: IrMatcher::Encoded carries a wildcard-aware pattern instead of a plain string, IrMatcher::Expand and CompiledMatcher::Expand carry the string operator in op, and HIR_SCHEMA_VERSION is 3, so Engine::load_hir rejects caches written by earlier versions and they must be regenerated. parse_expand_template and the value_placeholders and wildcard_placeholders transformations follow the new escaping rules.

Converted conditions are grouped by operator precedence (#530)

backend convert now parenthesizes nested conditions from the structure of the rule instead of from the rendered text, the way pySigma does. The PostgreSQL backend left an OR under an AND bare, so a selection with a value list and another field, such as Image|endswith: [a, b] with CommandLine|contains: x, rendered as "Image" ILIKE '%a' OR "Image" ILIKE '%b' AND "CommandLine" ILIKE '%x%' and matched any event with the first value. The same happened to 1 of selection_* under and and to add_condition pipeline transformations on rules with an or condition, while not 1 of filter_* and not over a selection with several fields negated only the first operand. The test backend had the same defects. The LynxDB backend, where OR binds tighter than AND, parenthesized every AND and left not 1 of filter_* and not (a or b) bare, so NOT applied to the first operand only. NOT is now parenthesized only over compound operands in every backend.

Backends control grouping through a new Backend::convert_condition_group hook, which receives the outer and inner operators. The default parenthesizes when the outer operator is NOT or binds tighter than the inner one, and text_convert_condition_group applies the TextQueryConfig precedence. The PostgreSQL engine workflow also converts every SigmaHQ rule whose condition needs grouping and that uses only plain string matches, synthesizes matching and non-matching events from the rule's own values, and requires PostgreSQL to agree with engine eval in both JSONB and typed-column mode. The LynxDB engine workflow runs the same check over the rules whose values use only characters LynxDB renders correctly. Engine test cases accept an optional pipeline applied before each engine's own pipelines.

Backend queries are tested in the engines that run them (#528)

The conversion backends now have tests that run the generated queries in the real engines instead of only comparing query text. A shared set of Sigma rules, each with sample events and the indexes of the events it must match, covers condition grouping, negation over missing fields, wildcards and escapes, case sensitivity, regular expression flags, numeric comparison, null, exists, and CIDR. Each case runs through engine eval, the PostgreSQL backend in PostgreSQL 18 (in both JSONB and typed-column mode), the LynxDB backend in a LynxDB v0.2.5 server, and the Fibratus backend in the Fibratus v3.1.1 filter engine (with and without macros, through the fibratus_windows pipeline). The test backend is compared textually with pySigma 1.5.1's TextQueryTestBackend. Confirmed defects record the exact mismatched indexes, engine error, or pair of differing outputs per case and engine. A defect that changes outcome or stops reproducing fails the test, so the fix must update or remove the record. Each engine has its own CI workflow that runs only when that backend, its harness, the shared cases, or shared parser, IR, pipeline, and conversion code changes. Contributors can run them locally as described in CONTRIBUTING.md.

fibratus_windows maps FileVersion to a field Fibratus defines (#528)

The fibratus_windows pipeline mapped FileVersion to process.pe.file.version for process_creation and process_termination, a field that does not exist in Fibratus, so the Fibratus loader rejected any converted rule that used it. It now maps to ps.pe.file.version.

Temporal correlations without a condition require every referenced rule (#524)

A temporal correlation that omits condition now fires only when every referenced rule matches within the timespan, as the Sigma correlation specification and pySigma define it. The parser used to default the threshold to gte: 1, so the first match of any single referenced rule fired the correlation. The default is now gte: <number of distinct rules>, which applies to engine eval, the daemon, and the PostgreSQL HAVING clause that backend convert emits. temporal_ordered already behaved correctly because its window value is non-zero only once every rule has fired in order. A temporal correlation with neither a condition nor any rules is now a parse error instead of a correlation that can never fire.

Temporal thresholds also count each distinct rule once: with rules: [a, a, b], a hit on a alone no longer counts as two rules toward gte: 2. Fixes #523.

Redrawn logo and a favicon set (#521)

The RSigma mark is redrawn from geometric curves in place of the traced bitmap: the same "rσ" silhouette and tiger stripes, with an even outline and a single orange gradient replacing 17 flat facets, in a file about an eighth of the size. A companion small mark (heavier outline, no stripes, flat orange) stays legible at 16 px. The assets/ directory now holds stacked and horizontal logotypes in light and dark variants as SVG, the README header uses the stacked SVGs, and the three logotype PNGs are removed. The docs sidebar shows the horizontal logotype instead of the mark beside CSS text, and the site serves an SVG favicon with a multi-size favicon.ico fallback and an Apple touch icon. The diagram headers use the new mark, with an outline that turns light in dark mode.

CLI command tree lists every subcommand (#520)

The CLI Reference command tree and quick-navigation table now match the binary: they add rule test, hunt run, and taxii store, follow the CLI's group order, and describe engine incidents export as exporting one incident's evidence bundle. The feature note adds taxii store to taxii-sync and notes that hunt run --emit events needs hunt-postgres.

Plain-text code blocks render without highlighting (#520)

Docs code blocks fenced as text (command trees, sample output, directory layouts) no longer pick up code coloring. The site's highlighter tokenizes every block the same way regardless of language, so an apostrophe such as the one in "daemon's" opened a colored "string" that ran to the next apostrophe; those blocks now render as escaped plain text.

Redraw the architecture diagrams (#520)

The ecosystem diagram now shows the daemon as it runs today: rules, pipelines, log events, dynamic sources, and TAXII-synced threat intel feeding the detection engine, enrichment (including stix lookups), the opt-in risk, alert pipeline, and disposition layers, and the state store, with the HTTP API, operator tooling, sinks (including webhook and OCSF output), and the paths outside the daemon (engine eval, backend convert, hunt run, rule authoring, MCP, LSP). The crate map is now a dependency graph of all nine crates, including rstix, drawn from the actual Cargo.toml edges. Both diagrams follow the reader's light or dark preference, and on the Architecture page every label links to the guide, reference, or CLI section it names. The page drops its Mermaid copy along with assets/architecture.mmd, and its dependency notes are corrected: rsigma-convert depends on rsigma-eval, rsigma-runtime depends on rstix, and the CLI does not depend on rsigma-lsp.

Docs: version tags, deployment guides, a loop tutorial, and troubleshooting (#519)

Every CLI command page, and every guide, reference, and library page for a feature added after v0.12.0, now carries an "Added in vX" tag linking to that release's notes; sections and table rows added since v0.22.0 are tagged too, and unreleased work shows an "Unreleased" tag until the release that ships it. The build fails when a tag names an unknown release or its link does not resolve.

New pages: Kubernetes and systemd deployment guides, Tutorial: The Detection Loop, which takes one rule through draft, test, deploy, triage, tune, measure, and hunt on sample data, and a symptom-first Troubleshooting page. The cloud collection recipes and the HTTP API reference are split into shorter pages by topic. In the detection loop diagram, each item in a list such as "lint · doc · LSP · MCP for AI agents" now links to its own page or section.

Fixes: the Docker, README, and combined cloud-recipe daemon examples now start (a non-loopback plaintext bind needs --allow-plaintext or TLS), the home page counts the workspace crates from Cargo.toml and lists every command group, and two cross-page anchors that did not resolve now do.

The site now builds with docmd 0.9.7. Its new AI chat plugin, which is on by default and sends reader questions to a third-party service, is turned off. Page titles in the new focus mode render inline code instead of raw backticks.

Interactive detection engineering loop diagram in the docs (#518)

On the Detection Engineering Loop guide, the diagram is now interactive: hovering a stage highlights its card, leader line, and ribbon node while the other stages fade, stage headers and nodes jump to the matching section, and each command or feature links to its CLI reference or guide page. The docs plugin copies assets/detection-loop.svg into the site at build time, embeds it with <object> (keeping the image as fallback), and rewrites its https://rsigma.io/ links to the configured base path so they follow the host serving the build. The README keeps rendering the same file as a static image.

Redraw the detection engineering loop diagram (#517)

The README and docs diagram now draws the loop as an infinity ribbon with a clean over-under crossing, numbered stage nodes that match numbered card headers, and consistent leader lines on both sides. Cards set CLI commands in monospace so they stand apart from feature names, the logo no longer disappears in dark mode, and assets/detection-loop.png is regenerated from the SVG.

Publish rstix before the crates that depend on it (#516)

The crates.io publish workflow now publishes rstix first, followed by an index wait. Since rsigma-runtime and rsigma gained an rstix dependency, publishing it last made the rsigma-runtime upload fail to resolve the new rstix version.

v0.23.0...v0.24.0