# BACKLOG - sigma (calyx-sigma)
Seeded 2026-07-14 (main-thread audit; Gemini swarm was quota/error-blocked). Sigma-rule → secir/Calyx template compiler. Condition parser is solid (depth-bounded, aggregation rejected loudly); the field/selection compiler has a correctness-breaking semantic drop plus silent severity/condition defaults. Rows: `file:line | category | severity | problem | acceptance criteria`.
DONE 2026-07-14 src/selection.rs:275 | correctness | high | `parse_field_match` did `let _field_name = parts.next().unwrap_or_default()` and never used it - the Sigma FIELD NAME was discarded. Every compiled matcher targeted `MatchPart::Body`, so a field-scoped rule like `EventID: 4625` or `CommandLine|contains: whoami` silently compiled to "the whole body contains 4625 / whoami" anywhere. | FIXED: `FieldMatch` now carries `field_name`; `as_simple_matcher` emits `MatchPart::Named(field)`; `dsl_clause` generates `regex(header("field"), "...")` for multi-field selections. `secmatch` DSL gained `header(name)` for case-insensitive named header lookup. Added `tests/audit_findings.rs` regression tests for single-field and multi-field scoping. `cargo test -p sigma`…
DONE 2026-07-14 src/parse.rs:303 | silent-fallback | medium | `map_severity` mapped any unrecognized level string to `Severity::Info` via `_ => Severity::Info` (test at parse.rs:332 baked this in). A typo'd or non-standard `level:` (e.g. `crtical`, `warning`) silently shipped the rule as Info, deprioritizing a real detection with no signal (Law 10 / product-truth). Sigma levels are a closed set. | FIXED: `map_severity` now returns `secir::Result<Severity>` and rejects unrecognized levels with `TemplateValidation`; the five spec levels (critical/high/medium/low/informational) plus the `info` shorthand remain accepted. Updated `parse.rs` unit test to assert `unknown` is rejected. `cargo test -p sigma` green. | status=done
DONE 2026-07-14 src/parse.rs:121 | silent-fallback | medium | `compile_rule` silently defaulted a missing `condition` to `"selection"` (`condition.as_deref().unwrap_or("selection")`). If the rule's selections weren't named exactly `selection`, it then errored confusingly; if they were, it silently guessed semantics the author never wrote. | FIXED: missing `condition` now defaults ONLY when there is exactly one selection (using that selection's name), and errors otherwise. `test_missing_condition_single_selection_defaults` and `test_missing_condition_multiple_selections_rejected` in `tests/break_it.rs` cover both branches. `cargo test -p sigma` green. | status=done
DONE 2026-07-14 | capability | medium | The modifier match handles only `contains/startswith/endswith/re/all/base64/base64offset/cased`; every other modifier hard-errors ("unsupported Sigma modifier"). Common real-world modifiers - `|cidr`, `|lt`/`|gt`/`|lte`/`|gte`, `|windash`, `|wide`/`|utf16`/`|utf16le`/`|utf16be`, `|fieldref`, `|exists` - cannot compile, so a large fraction of upstream SigmaHQ rules fail. (Loud, not silent, but a coverage gap.) | IMPLEMENTED `|windash`: each `-` in a value is expanded to a regex character class `[-/–-―]` covering Windows `-`, `/`, en dash, em dash, and horizontal bar. `|windash` forces the `|contains` fast path to fall back to `MatcherKind::Regex` so the class is preserved, and it works with `Exact`/`Startswith`/`Endswith`. The unsupported-modifier error now lists supported modifiers (contains, startswith, endswith, re, all, base64offset, base64, cased, windash) and explicitly names the not-yet-implemented set (cidr, lt/gt/lte/gte, wide/utf16 variants, fieldref, exists) so callers see the boundary. Added `verify_windash_contains_uses_regex_with_dash_class`, `verify_windash_exact_anchors_dash_class`, and `verify_unsupported_modifier_error_lists_supported_and_gap` in `tests/audit_findings.rs`. `cargo +1.92.0 test` (from `libs/scanner/sigma`, its own workspace) passes.
DONE 2026-07-14 src/selection.rs:79 | correctness | medium | `|contains` with default case-insensitivity compiles to `MatcherKind::Word`; secir's `Word` matcher uses `AhoCorasick` with `ascii_case_insensitive(true)` and the `Named` header path uses `contains_ascii_case_insensitive`, so the default `|contains` is already case-insensitive. The multi-field DSL path was using `contains(body, ...)` which is case-sensitive. | FIXED: `dsl_clause` for `Contains` now emits `regex(header("field"), "(?im)...")` (flags include `(?i)`), so both the single-field `Word` path and the multi-field DSL path are case-insensitive. `verify_endswith_is_case_insensitive` and `verify_startswith_is_case_insensitive` audit tests already cover header case-insensitivity; `cargo test -p sigma` green. | status=done
DONE 2026-07-14 | src/condition.rs:366 | test-gap | medium | `parse_expr_inner` accepts `| near` (builds `ConditionExpr::Near`) and `parse_of_expression` accepts `N of pattern` / `all of them`, but there is no test proving `flatten_condition` lowers Near (temporal correlation) or wildcard `of`-patterns correctly into matchers. A parsed-but-mis-lowered correlation silently produces wrong matcher logic. | add tests asserting `1 of selection*`, `all of them`, and `a | near b` compile to the intended matcher set/condition (assert the matchers, not just Ok). | FIXED: added `condition::tests` unit tests directly exercising `parse_condition` and `flatten_condition`: `flatten_1_of_them_is_or`, `flatten_all_of_them_is_and`, `flatten_1_of_pattern_selects_matching_selections`, `flatten_all_of_pattern_selects_matching_selections`, `flatten_near_is_rejected`, `flatten_and_condition_is_and`, `flatten_or_condition_is_or`. These pin that `1 of them`/`1 of sel*` compile to `MatcherCondition::Or`, `all of them`/`all of sel*` compile to `MatcherCondition::And`, and `sel1 | near sel2` is rejected with a clear error. `cargo +1.92.0 test -p sigma` CARGO_EXIT=0. | status=done
DONE 2026-07-14 | src/parse.rs:102 | data | low | The Sigma reserved-detection-key set `["condition","timeframe","keywords"]` is a hardcoded inline array. It is spec-defined and small, but embedding it inline (Tier-B-shaped) means a spec addition is a source edit with no single owner. | hoist to a named `const` with a doc citing the Sigma spec section, so the reserved set has one owner; consider whether `keywords` should be compiled rather than merely reserved-and-dropped. | FIXED: hoisted the reserved set into `SIGMA_RESERVED_DETECTION_KEYS` in `src/parse.rs` with a doc comment citing the Sigma spec "Detection" section and noting `keywords` is reserved-not-compiled. The inline array in `compile_rule` now refers to the const. Added `parse::tests::reserved_detection_keys_are_excluded_from_selections` covering `condition`, `timeframe`, and `keywords` alongside a real selection. `cargo +1.92.0 test -p sigma --lib` green (CARGO_EXIT=0).
DONE 2026-07-14 tests/test_depth_{adversarial,concurrent,fault,integer}.rs | test-gap | medium | `test_depth_*` integration tests had stale expected `template.id` (title-based `title_to_id` slug mismatched), expected raw `values[0]` for `Exact` matchers instead of the anchored regex pattern, and one test expected an empty-string matcher to compile. | FIXED: aligned YAML titles with `title_to_id` output; updated `Exact` matcher assertions to match `(?im)^<value>$` patterns; changed empty-string test to assert `is_err()`; `cargo test -p sigma` green. | status=done
DONE 2026-07-16 src/condition.rs:305 | sigma | performance | low | REPOPULATE 2026-07-16: wildcard_matches calls to_ascii_lowercase on the constant pattern for every candidate name iteration, allocating a new String on each check and causing needless allocation overhead. | documented contract that pattern is converted to lowercase once before the filter loop + a proving test asserting case-insensitive wildcard matching correctness | FIXED (Law-7): extracted `wildcard_matches_lowered(pattern_lc, candidate)` which takes an already-lowercased pattern (documented contract). `resolve_selection_pattern` now branches once - for a wildcard pattern it lowercases the pattern ONCE (`let pattern_lc = pattern.to_ascii_lowercase()`) before the filter loop and calls the lowered helper per candidate, instead of re-lowercasing the constant pattern for every candidate name (was one String alloc per candidate). The non-wildcard branch uses `eq_ignore_ascii_case` with no allocation. `wildcard_matches` keeps its single-shot signature by delegating to the helper. Proving tests condition::tests::resolve_wildcard_pattern_is_case_insensitive_after_hoisting_lowercase (mixed-case pattern selects exactly the matching candidates, sorted) and wildcard_matches_lowered_equals_wildcard_matches (behavior-identical to the entry point across 6 mixed-case pairs). Gate: cargo test (crate dir) all suites passed, EXIT=0. | status=done
DONE 2026-07-16 src/selection.rs:227 | sigma | performance | low | REPOPULATE 2026-07-16: windash_escaped calls regex::escape on every single non-dash character, allocating a new String per character, causing high allocation rates and compilation slowdown for rules using windash. | documented contract of single-allocation regex escaping or character-by-character non-allocating escaping + a proving test asserting windash characters are escaped properly | FIXED (Law-7): windash_escaped now escapes each MAXIMAL non-dash run in a single `regex::escape` call (tracking `run_start`/`char_indices`, flushing the pending run when a `-` is hit and after the loop) instead of one `regex::escape` + temp String per character. Documented that `regex::escape` is context-free per character, so escaping a run and concatenating is byte-identical to the old per-char form while every `-` still maps to the WINDASH_CLASS. Proving test tests/audit_findings.rs::verify_windash_escapes_metachar_runs_and_maps_every_dash compiles `CommandLine|windash: "-a.b-c-"` and asserts the exact regex `(?im)^[-/–-―]a\.b[-/–-―]c[-/–-―]$` (leading dash, trailing dash, and a `.` metachar inside a batched run). Gate: cargo test (crate dir) all suites passed, EXIT=0. | status=done
DONE 2026-08-07 src/selection.rs:160, src/condition/parser.rs:175 | correctness | high | `SigmaOp::Regex` omitted case-insensitivity flags (`(?im)`) and raw regex syntax validation; `parse_of_expression` rejected `any of ...` quantifiers. | FIXED: `regex_pattern_for_value` now prepends case-flags (`(?im)` default, `(?m)` if `cased`), expands `windash`, and validates raw regex patterns with `Regex::new`; `parse_of_expression` now parses `any of` as `Some(1)`. Added proving tests in `tests/audit_findings.rs`. | status=done
DONE 2026-08-07 src/condition/mod.rs:270 | correctness | medium | `wildcard_matches_lowered` performed sequential `find()` calls for pattern parts without properly anchoring non-`*` suffix parts at the end of candidate strings (e.g. `a*a` against `a_a_x_a` or `a*b*a` against `a_b_a_b_a` matched an intermediate part and left a non-empty remainder, returning false). | FIXED: refactored `wildcard_matches_lowered` to handle prefix, suffix (anchored when pattern does not end with `*`), and middle parts in sequence cleanly. Added unit tests in `src/condition/tests.rs`. | status=done
DONE 2026-08-07 src/selection.rs:212, src/condition/parser.rs:167 | capability | medium | Selection compiler rejected sequence of mappings (`- Image|endswith: ...`) and sequence of scalars (`- cmd.exe`); condition parser rejected parenthesized pattern lists (`1 of (selection_*)`, `1 of (sel1, sel2)`, `all of (them)`) and `at least N of ...` quantifiers. | FIXED: `compile_selection` now compiles sequence of maps to DSL OR clauses and sequence of scalars to Word/Body matchers; `parse_of_expression` and `tokenize_condition` now parse parenthesized pattern lists, comma-delimited selection sets, and `at least 1 of` quantifiers. Added proving tests in `tests/audit_findings.rs`. | status=done