Detection Engineering

What Is a Sigma Rule Catalog and Why Your SOC Needs One

HuntRule Team · · 9 min read

An archivist's card index drawer pulled open in the dark, blank unmarked cards standing in rows behind metal tabs
On this page

On 2023-12-15 SigmaHQ changed the rule carrying id 5cc90652-4cbd-4241-aa3b-4b462fa5a248. The change added dnsgetdc: to the flag list and stripped the leading slash from most of the other strings, so /domain_trusts became domain_trusts and started matching nltest.exe -domain_trusts too. If you exported that rule into Splunk in 2022 and never went back, your saved search still carries the 2022 logic.

That is the problem a catalog solves. It is not "where do I get rules". SigmaHQ publishes more than 3000 of them and the download costs nothing. The problem starts the moment a rule leaves the repository.

The import that never gets a second version

Most SIEM onboarding of Sigma looks like this. Clone the repo, run a conversion, paste the queries into the console.

sigma plugin install splunk
sigma convert -t splunk -p sysmon sigma/rules/windows/process_creation

That command is correct and it works. It is also a one-way door. The output is a pile of SPL with no id, no author, no related block and no record of which commit produced it. Six months later upstream has changed 200 of those rules and you have no mechanism that can tell you which 200, because the artifact in your SIEM shares no identifier with the artifact in the repo.

SigmaHQ ships rule packages precisely to make ingestion repeatable. The Core package is stable and test status rules at high and critical level. Core+ adds medium, which is where most environment-specific tuning lands. Core++ adds experimental. Picking a package fixes your noise appetite. It does not fix the upgrade path, because the package is still just a zip of YAML you have to reconcile against whatever you did to the last one.

A rule that moved after you copied it

Here is the rule from the opening, verbatim from rules/windows/process_creation/proc_creation_win_nltest_recon.yml.

title: Potential Recon Activity Via Nltest.EXE
id: 5cc90652-4cbd-4241-aa3b-4b462fa5a248
related:
    - id: 410ad193-a728-4107-bc79-4419789fcbf8
      type: similar
    - id: 903076ff-f442-475a-b667-4f246bcc203b
      type: similar
    - id: 77815820-246c-47b8-9741-e0def3f57308
      type: obsolete
status: test
description: Detects nltest commands that can be used for information discovery
author: Craig Young, oscd.community, Georg Lauenstein
date: 2021-07-24
modified: 2023-12-15
tags:
    - attack.discovery
    - attack.t1016
    - attack.t1482
logsource:
    category: process_creation
    product: windows
detection:
    selection_nltest:
        - Image|endswith: '\nltest.exe'
        - OriginalFileName: 'nltestrk.exe'
    selection_recon:
        - CommandLine|contains|all:
              - 'server'
              - 'query'
        - CommandLine|contains:
              - '/user'
              - 'all_trusts'
              - 'dclist:'
              - 'dnsgetdc:'
              - 'domain_trusts'
              - 'dsgetdc:'
              - 'parentdomain'
              - 'trusted_domains'
    condition: all of selection_*
falsepositives:
    - Legitimate administration use but user and host must be investigated
level: medium

Note modified: 2023-12-15. The file has been touched since. A 2024-07-02 commit rewrote the Microsoft reference URL from the docs subdomain to learn, and a 2024-08-12 commit changed the related type from obsoletes to obsolete for Sigma spec v2. Neither bumped modified, because the spec only asks for a bump on title, detection, level, logsource or a status change to deprecated. So modified is a signal about detection semantics, not a change detector. If your sync logic keys on modified, it will miss real file changes. Hash the file or track the commit.

Two identical translucent blueprints overlaid on a dark drafting table, slightly out of register so their lines double and split at the edges

What a local tune actually looks like

Assume a lab environment where an inventory agent runs nltest /dclist: on a schedule and buries the analyst. The obvious move is to edit the YAML in place. Below is a reconstruction of that edit, shown as the changed hunks of local/rules/windows/process_creation/proc_creation_win_nltest_recon.yml against the upstream copy of the same path.

@@
 id: 5cc90652-4cbd-4241-aa3b-4b462fa5a248
-status: test
+status: stable
-modified: 2023-12-15
+modified: 2026-05-04
@@
               - 'parentdomain'
               - 'trusted_domains'
-    condition: all of selection_*
+    filter_main_inventory:
+        ParentImage: 'C:\Program Files\Contoso\inventory-agent.exe'
+        CommandLine|contains: '/dclist:'
+    condition: all of selection_* and not 1 of filter_*
 falsepositives:
     - Legitimate administration use but user and host must be investigated
-level: medium
+level: low

Four lines of YAML and the rule is now a fork wearing upstream's id. Nothing in the file records that it was forked, when, by whom, or on what evidence the level was dropped. Pull the next release and one of three things happens. Your sync overwrites the tune. Your sync skips the file and you stop getting upstream improvements forever. Or your sync raises a conflict that a human has to resolve with no context about why the local side looks like that.

The spec is explicit about the id question. A change to detection logic warrants a new id, with the origin recorded in related as type: derived. Almost nobody does this, because there is no place to put the rest of the story.

The metadata a catalog has to carry

A catalog is the place to put the rest of the story. Per rule, at minimum:

Field

Example value

What it answers

upstream_id

5cc90652-4cbd-4241-aa3b-4b462fa5a248

Which SigmaHQ rule is this descended from

upstream_ref

commit sha, path, package Core+

Exactly which version was copied, and from where

local_id

fresh UUIDv4

This is our rule now, not theirs

local_diff

filter_main_inventory added, level medium to low

What we changed, and nothing else

tuning_reason

inventory agent runs /dclist: hourly

Why the diff exists, in one sentence

validated_on

2026-05-04, Atomic Red Team T1482 test 2

Who proved it fires, against what, when

backends

splunk (sysmon), elasticsearch (ecs_windows)

The conversion target is part of the rule

data_source

Sysmon EID 1, or Security 4688 with command line auditing

Which log actually feeds it

owner

detection engineering

Who answers the page

review_due

2026-11-04

The drift check has a date on it

data_source is the field teams skip and regret. This rule needs OriginalFileName, which Sysmon Event ID 1 populates and Security Event ID 4688 does not. Deploy it against 4688 only and the renamed-binary branch of selection_nltest is dead weight, and you will not know unless the mapping is written down somewhere a person reads.

validated_on is the review problem in a single row. Ask a SOC who validated a given rule, against what test, and on what date. If the answer lives in a closed ticket or a Slack thread, it does not exist. Sigma covers the basics of what a rule is, and our primer on Sigma rules walks the format. Provenance is the part the format leaves to you.

Tuning without forking

Sigma has a mechanism most teams have not adopted. A global filter is a separate YAML document that attaches an exclusion to named rule ids without touching the rule files.

title: Filter inventory agent domain enumeration
id: 0e95725d-7320-415d-80f7-004da920fc11
description: Contoso inventory agent runs nltest /dclist: hourly on all DCs
logsource:
    category: process_creation
    product: windows
filter:
    rules:
        - 5cc90652-4cbd-4241-aa3b-4b462fa5a248
    selection:
        ParentImage: 'C:\Program Files\Contoso\inventory-agent.exe'
    condition: not selection

Upstream rule files stay byte-identical to the release, so git diff against a new package is clean and the whole tune lives in one reviewable file. The tradeoff is real. Filter support depends on your tooling version, it arrived in pySigma 0.11.7 and sigma-cli 1.0.3 in June 2024, and a filter cannot express a level change or a detection widening. Forks are still necessary sometimes. The catalog's job is to make the fork visible, not to pretend it never happens.

What the rule catches and what it misses

The rule fires when a process ends in nltest.exe, or reports nltestrk.exe as its OriginalFileName, and the command line contains both server and query, or any one of the discovery flags. That maps to T1482 and T1016.

It misses domain trust enumeration that never calls nltest. Get-ADTrust, raw LDAP from System.DirectoryServices, and any custom binary calling DsEnumerateDomainTrusts all produce the same intelligence and none of the command lines. It also misses a copy of nltest.exe with the PE version resource stripped, executed under an unrelated name, on a sensor that only gives you 4688.

Known false positives are ordinary. Domain join troubleshooting, replication health scripts, asset inventory, and helpdesk runbooks all call these flags. That is exactly why the rule ships at medium and lands in the Core+ package rather than Core.

Related detections live under Windows rules in the catalog, and discovery-stage coverage is searchable at /rules?q=discovery.

Git alone is often enough

Do not build a catalog because a blog post said so. A two-person team with one SIEM and 40 active rules gets most of the value from a repo laid out as upstream/ (a pinned SigmaHQ release, never edited), local/ (your rules and filters), and a CI job.

sigma check local/
sigma convert -t splunk -p sysmon local/ -o build/splunk.conf
git diff upstream/

That gives you version history, a diff against upstream, syntax validation and reproducible builds. It does not give you review dates, per-rule ownership, validation evidence, or a query that answers "which rules cover Windows credential access and have not been reviewed this year". Those are what the extra metadata buys, and the price is that somebody has to fill the fields in and keep them honest.

The break-even point is roughly: more than one conversion backend, more than one team writing rules, or an auditor who asks questions. Below that, a git repo and a discipline of writing the tuning reason in the commit message is the correct answer, and adding a catalog first is overhead with no payer.

Summary

A Sigma rule catalog exists to answer questions the YAML cannot: which upstream rule this came from, what we changed and why, who proved it works and when, which log source feeds it, and when it gets looked at again. The raw import into a SIEM answers none of these, and the modified field is not a reliable change detector because SigmaHQ does not bump it for reference or schema edits. Global filters let you tune common noise without forking, which keeps the upstream diff clean. Small teams can get most of the benefit from a pinned upstream directory plus sigma check in CI, and should start there.

Fields to record per rule before you call it managed:

upstream_id       Sigma UUID the rule descends from
upstream_ref      commit sha + path + release package
local_id          new UUIDv4 if detection logic changed
local_diff        summary of every deviation from upstream
tuning_reason     one sentence, why the diff exists
validated_on      date + test used (e.g. Atomic Red Team T1482)
backends          conversion targets and pipelines
data_source       Sysmon EID / Windows Security EID / EDR table
owner             team that answers the alert
review_due        next drift and validation check

Related articles