rsigma rule hygiene🔗
Flag rule hygiene and retirement candidates in one report, the lifecycle phase a mature detection program reviews on a retirement cadence.
Synopsis🔗
Description🔗
rule hygiene assembles the raw signals rsigma already produces into a single report of retirement and clean-up candidates. The 2026 detection-engineering maturity guidance treats retirement as a first-class discipline: every detection needs an owner, a last-fired date, and a deletion bar, and the rule catalog grows until the team drowns unless something drives the cull. A rule that has not fired in a year, or fires only on false positives, is a deletion candidate. This command surfaces those candidates.
It runs no evaluation against the rules. The static signals read straight off the parsed rules; the silence and noisy signals join a Prometheus snapshot or endpoint; the broken-coverage signal joins a field-observability snapshot. It is an offline rule-group command with no engine or hot-path involvement.
Only --rules is required. The static signals (untagged, no-owner, incomplete-ads, deprecated/stale) report from the rules alone; the silence and noisy signals need --metrics; the broken-coverage signal needs --fields.
Signals🔗
| Signal | Source | What it flags |
|---|---|---|
silent | --metrics | A rule with no matches in the snapshot, or one whose last-fired (with --metrics-window) is older than --silent-threshold. |
noisy | --metrics | A fire-count outlier over the window: a robust median-plus-MAD test by default, or any rule at or above an absolute --noisy-threshold. |
untagged | --rules | A rule carrying no attack.* ATT&CK tag. This is the same notion of "untagged" rule coverage reports, computed by the same shared extractor. |
no-owner | --rules | A rule with no owner: no custom_attributes owner key and no author. |
incomplete-ads | --rules | A stable detection rule, not ADS-exempt, that is missing at least one required ADS section. Mirrors the default bar of the ADS presence lint; finer control stays in the linter. |
broken-fields | --fields | A detection rule whose referenced fields are all in the snapshot's never-seen (missing) set. |
deprecated | --rules | A rule with status: deprecated or status: unsupported, or a modified/date older than --stale-threshold. |
Inputs🔗
| Input | Flag | Required | What it supplies |
|---|---|---|---|
| Rules | --rules <PATH> | yes | The rule set to report on (repeatable; file or directory). |
| Prometheus snapshot or endpoint | --metrics <FILE\|URL> | no | Per-rule fire volume from rsigma_detection_matches_by_rule_total and rsigma_correlation_matches_by_rule_total, joined by rule_title. Drives silent and noisy. |
| Prometheus query API | --metrics-window <DURATION> | no | When --metrics is a Prometheus query-API base, switches to a query_range over the window to derive a true last-fired timestamp. |
| Event corpus | --corpus <PATH> | no | The offline alternative to --metrics (no daemon, no Prometheus): a file or directory replayed through the engine for per-rule fire counts. Combined with --metrics, the counts are summed. Also drives silent and noisy. A --corpus path that contains no readable files exits 3 rather than marking every rule silent. |
| Field-observability snapshot | --fields <FILE> | no | The /api/v1/fields payload (or its missing array) from a daemon with --observe-fields, or the rsigma engine eval --observe-fields report. Drives broken-fields. |
At least one of --metrics or --corpus is required for the silent and noisy signals; the static signals need only --rules.
The Prometheus join inherits the caveat documented in Metrics: rule_title is not guaranteed unique, so when two rules share a title their counters add together. The shared reader is the same one rule scorecard uses.
The broken-coverage rollup needs each rule's full referenced-field set, so it joins the snapshot's missing field names against the fields extracted from --rules. Generate the snapshot from the same rule set so the field names line up.
Flags🔗
| Flag | Default | Description |
|---|---|---|
--rules <PATH> | required | Sigma rule file or directory (repeatable). May also be supplied via hygiene.rules. |
--metrics <FILE\|URL> | unset | A Prometheus exposition snapshot file or a /metrics URL. May also be supplied via hygiene.metrics. |
--metrics-window <DURATION> | unset | Range-query window (e.g. 7d, 24h) when --metrics is a query-API base. May also be supplied via hygiene.metrics_window. |
--corpus <PATH> | unset | Event corpus file or directory replayed for offline fire counts (repeatable). |
--input-format <FORMAT> | auto | Input log format for non-NDJSON corpus files (json, syslog, plain, logfmt, cef, auto). Only used with --corpus. |
--fields <FILE> | unset | A field-observability JSON snapshot. May also be supplied via hygiene.fields. |
--silent-threshold <DURATION> | 365d | Age past which a never-fired rule is a retirement candidate. May also be supplied via hygiene.silent_threshold. |
--stale-threshold <DURATION> | 365d | Modified-date age past which a rule is flagged stale. May also be supplied via hygiene.stale_threshold. |
--noisy-threshold <COUNT> | unset | Absolute per-window fire ceiling that overrides the robust outlier test. May also be supplied via hygiene.noisy_threshold. |
--report <FILE> | unset | Write the full JSON report to disk, independent of --output-format. |
--fail-on <CONDITION> | unset | Exit 1 when a selected finding matches at least one rule (repeatable): silent, noisy, untagged, no-owner, incomplete-ads, broken-fields, deprecated, or any. May also be supplied via hygiene.fail_on. |
--config <PATH> | unset | Load a specific YAML config file instead of running the discovery chain. |
--dry-run | off | Print the effective hygiene section and exit 0 without running. |
The global --output-format applies: table (the TTY default) renders the flagged rules under a per-signal summary, json emits the full report document, and ndjson/csv/tsv emit one row per flagged rule.
Report🔗
The JSON document (--output-format json) has a stable shape:
summary: total, detection, and correlation rule counts, the flagged count, which sources contributed, and a per-signal count.rules[]: per flagged rule, its title, id, kind, the signals it tripped, fire count and last-fired where known, owner, status, and tags.- the per-signal lists
never_fired,noisy,untagged,no_owner,incomplete_ads,broken_coverage, andstale_status.
--report writes the same JSON document to a file regardless of the chosen output format.
Exit codes🔗
| Code | Meaning |
|---|---|
0 | Success, or findings were produced but none tripped --fail-on. |
1 | --fail-on was set and at least one selected condition matched. |
2 | The rules could not be loaded. |
3 | A bad flag, or an unreadable or malformed metrics/fields input. |
Examples🔗
Report from the rules alone (static signals)🔗
Add production fire volume for silence and noise🔗
Use a replayed corpus instead of Prometheus (offline)🔗
Add broken field coverage from a field-observability snapshot🔗
rsigma rule hygiene --rules ./rules \
--metrics http://localhost:9090/metrics \
--fields fields.json
Gate CI on rules silent longer than a year🔗
rsigma rule hygiene --rules ./rules --metrics metrics.txt \
--silent-threshold 365d --fail-on silent
See also🔗
- Rule Hygiene for the retirement-cadence workflow and how each input feeds each signal.
rule coverageshares the ATT&CK tag extraction behind the untagged signal;rule scorecardis the quantitative keep/tune/retire verdict this report complements.- Observability for generating the field-observability snapshot.
- Configuration for the
hygieneconfig section. - Exit Codes reference for the canonical table.