Repository navigation
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_VERSIONis 3, processing-pipeline and correlation APIs change, and the PostgreSQLcase_sensitive_reoption 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
logsourceis missing, or sets none ofcategory,product, andservice, or one of those keys ordefinitionis not a string. Filter rules need alogsourceas well, as the Sigma filter specification requires. - a field combines conflicting modifiers, such as two operators (
|gt|lt), two UTF-16 encodings,base64withbase64offset, or a string modifier beforefieldref. - a value has the wrong type for its modifiers: a number or boolean under
contains,startswith,endswith,re,cidr, an encoding,fieldref,expand, orcased, or anything but a YAML number undergt,gte,lt,lte, or a timestamp part such asminute. - a
revalue is not a valid regular expression, acidrvalue is notaddress/prefixor has host bits set, afieldref,base64, orbase64offsetvalue contains a wildcard, or a UTF-16 encoding withoutbase64orbase64offsethas a non-ASCII value. existsis applied to a keyword or takes anything but a single YAML boolean,trueorfalse. Previously any other value, such as'yes'ormaybe, was treated astrue.|allhas 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 a1 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
logsourcewith at least one ofcategory,product, orserviceto every detection and filter rule. A filter applies only to rules whoselogsourceincludes every key the filter sets, so give a filter only the keys its rules share, such asproduct: windows. - Fix or remove values that the new checks reject. Run
rsigma rule validateon the rules directory to list them; every message names the field and the problem. - Pass a
--logsource-*flag torule 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
|ito arevalue that should keep matching case-insensitively in PostgreSQL. Thecase_sensitive_rePostgreSQL option is removed because plainreis 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 valuessearchcannot match exactly now start withFROM <index> | whereinstead ofFROM <index> | search, and scan every event in the index. Saved queries that parse thesearchexpression 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_stringand field-name regexes match from the start of the text.match_stringsees the value withcontains,startswith, andendswithwildcards added and literal wildcards escaped, never matches field references or other non-string values, appliesnegateper value, and acceptscond: any|all.is_nullacceptscondtoo.- Field-name conditions select a detection item through its field name or any of its field reference targets, for every transformation, including
drop_detection_itemanddetection_item_failure. include_fieldsandexclude_fieldsread pySigma'smode: plain|rekey, withmatch_typekept as an alias.processing_item_appliedtracks renamed fields at field-name scope and changed items at detection-item scope instead of reporting whether the transformation ran anywhere in the rule.nestapplies each inner item under its own conditions only, and correlation rules honor field-name conditions,nest, andfield_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
|expandto values whose placeholders a pipeline should expand; plain values and keywords are no longer expanded. - Set
allow_unresolved: trueonvalue_placeholdersto keep runtime substitution for variables the pipeline does not define. hashes_fieldsno longer prefixes field names withFileby default and no longer splits comma-separated values. Setfield_prefix: Fileto keep the previous field names.- Add
cond: allwhere amatch_stringoris_nullcondition must hold for every value.
Breaking changes for library users:
TransformationItemstores each condition scope inConditionSet<T>instead of separate vectors and flags. Access parsed conditions through.conditionsand use one-based string identifiers for positional conditions. The legacyNamedRuleConditiontype andall_rule_conditions_matchhelper remain available for callers that only need AND linking.- Each
ProcessingStatecondition carries a typedvaland aStateOperator; setStateOperator::Eqfor the previous equality behavior.Transformation::SetState.valueis aserde_json::Value; wrap strings withserde_json::Value::String. DetectionItemCondition::MatchStringandIsNullcarry acond: ValueMatch;ValueMatch::Anykeeps the previous linking.Transformation::AddConditiongainsnameandtemplate,ReplaceStringgainsinterpret_special,HashesFieldsgainsfield_to_parse, and the placeholder variants carry their new options. UseNone,false, andvec!["Hashes".into(), "Hash".into()]for the previous behavior.PipelineStatereplaces the publicdetection_item_appliedset,reset_detection_item(), andwas_applied_to_detection_item()withdetection_item_was_processed_by(), and addsfield_name_applied,track_field_rename(), andfield_was_processed_by().eval_condition_exprreturnsfalsefor an expression that does not parse, such as an empty string.Transformation::applywith an empty field-name condition list andfield_name_cond_not: trueno 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
nameeven when they have noid, including through schema routing and chained correlations. - Events missing any
group-byfield, 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 validatereports correlation references that resolve to no ruleidorname.- Referenced detection matches are no longer standalone output by default. Top-level
generate: trueon a correlation emits its referenced detections, whileengine evalandengine daemonaccept--emit-detectionsto emit every detection match. The deprecated--no-detectionsflag remains accepted. Daemon configuration usescorrelation.emit_detections, with the inverseno_detectionskey retained for compatibility. The MCPevaluate_eventstool exposes the same opt-in behavior. - Correlation-capable conversion omits referenced standalone rules unless at least one referencing correlation has
generate: true. rule lintemits the informationalcorrelation_only_referenceshint when a correlation omitsgenerate: 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
|alllist, 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|neqholds 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
4624never matched. Keyword numbers now match their decimal text, and the keyword search covers numeric event values. - A selector pattern such as
1 of *orall of sel*also matched detections whose names start with an underscore. Only patterns that start with an underscore select them now, asthemalready 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
startswithorendswithwas anchored at the wrong end, soCommandLine|startswith: 'net*user'matchedcmd /c net userand missednet localuser /add. Wildcards now also match newlines. base64offsetkept a character that depends on the byte after the value, soData|base64offset|contains: Testmissed the encodings ofTesting. It now trims each variant the way pySigma does.windashonly 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), andwindashcombined withbase64encodes every variant instead of being ignored.wide,utf16, andutf16bewithoutbase64matched the plain text. They now match the UTF-16 string, and reject non-ASCII values.- A wildcard under
base64orbase64offset, which an encoding cannot represent, is rejected instead of being encoded as a literal star. - An
expandvalue always compiled to whole-value equality, ignoringcontains,startswith,endswith, and wildcards from pipeline variables. A value whose placeholders a pipeline resolved now matches like any other value and converts withbackend convert; placeholders left for match time honor the string operator. A backslash now escapes%as the specification defines, soC:\Users\%user%must be writtenC:\Users\\%user%to keep the placeholder. existstreated a field set tonullas absent. It is now a presence check.neqnever matched an event without the field, soUser|neq: adminskipped events with noUser. It now matches missing and null fields, and the PostgreSQL backend no longer addsIS NOT NULLto 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.