Skip to content

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🔗

rsigma rule hygiene --rules <PATH>... [OPTIONS]

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, and stale_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)🔗

rsigma rule hygiene --rules ./rules

Add production fire volume for silence and noise🔗

rsigma rule hygiene --rules ./rules --metrics http://localhost:9090/metrics

Use a replayed corpus instead of Prometheus (offline)🔗

rsigma rule hygiene --rules ./rules --corpus ./corpus

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🔗