Two teams read the same threat report on the same Tuesday and write the same detection. One writes it as a Splunk search, the other as a KQL query. Both are correct, neither is any use to the other, and both will be rewritten in six months when a field name changes.

A Sigma rule is a short YAML file describing what to look for in log data, without saying which product should do the looking. A converter turns it into the query language of whatever you run. That portability is why an unfamiliar YAML file so often turns up attached to a vulnerability write-up: it is the closest thing the industry has to a shared notation for a detection.

What follows is a walk through the current specification, version 2.1.0, published on 2 August 2025, and the parts that reliably trip people up. If you have a rule in front of you, our Sigma rule linter checks one in the browser.

What Sigma is actually for#

Sigma occupies the same slot for log data that Snort rules occupy for network traffic and YARA rules occupy for files. It is a notation, not an engine: nothing executes a Sigma rule directly, and the query you convert it into is what your platform runs. That separation buys portability, and also reviewability, which matters more day to day. A detection written as forty lines of vendor query language is hard to argue about in a pull request; the same detection as fifteen lines of YAML can be corrected by someone who has never touched that console.

The anatomy of a rule#

A complete rule is smaller than most people expect:

title: Scheduled Task Registered With An Encoded Command
id: 8f8d2a3e-1c4b-4f2a-9f0e-2b7c6d5a1e34
status: experimental
description: Detects a scheduled task registered with a command line
  that runs an encoded PowerShell payload.
references:
  - https://attack.mitre.org/techniques/T1053/005/
author: Network Lookup
date: 2026-08-24
tags:
  - attack.persistence
  - attack.t1053.005
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4698
    TaskContent|contains:
      - '-enc '
      - '-EncodedCommand'
  filter_service_accounts:
    SubjectUserName|startswith: 'svc_'
  condition: selection and not filter_service_accounts
falsepositives:
  - Deployment tooling that registers tasks with encoded arguments
level: high

Only three of those keys are required: title, logsource and detection. Everything else is optional, which surprises people who have only seen rules from curated repositories where house style fills all of it in. Inside detection, condition is mandatory. title is capped at 256 characters.

logsource: narrowing the haystack#

The logsource block says which stream of events the rule expects, using three attributes converters act on: category (a generic class such as process_creation), product (the platform, such as windows), and service (a subset, such as security). An optional definition explains what has to be logged for the rule to have any chance, which is the field people skip and later wish they had read.

The important thing is what logsource does not do. It names no index, table or log path. It is a label a conversion pipeline maps onto wherever those events live in your environment, so service: security describes the Windows Security log conceptually without pointing at anything. For what that log actually produces, our Windows Event ID decoder and the guide to the Security log cover the numbering.

detection: selections, and how they combine#

Inside detection you define one or more search identifiers. The names are yours; selection and filter are convention, not syntax. Each is either a map of field/value pairs or a plain list, and two combination rules govern everything:

  • Keys within one map are joined with AND. A selection listing three fields matches only events where all three hold.
  • Values in a list under one key are joined with OR. The example above matches a task containing -enc or -EncodedCommand.

An identifier that is a bare list of strings rather than a map applies to the full log message, OR'd, which is the escape hatch for unstructured sources.

Getting those backwards is the most common logic error in the format. A rule needing "field A is X or field B is Y" cannot say it in one map; it needs two identifiers and a condition that ORs them. Writing both fields into one selection produces a narrower rule that still converts, still runs, and simply fires less often, which is the failure mode nobody notices.

Modifiers, where most of the power lives#

Anything after a pipe in a field name is a value modifier, and modifiers are where a flat equality match becomes a real detection. The specification defines a fixed list. The ones you will meet constantly: contains, startswith, endswith, all (which flips a list from OR to AND), exists, cased, and re for a regular expression.

Others solve specific evasion problems. windash generates the permutations of dash and slash characters that Windows command-line flags accept, so /enc is caught alongside -enc. base64offset handles the three possible byte alignments of a string embedded in a larger Base64 blob, which is the difference between catching an encoded payload and catching only the ones that start on a boundary. Alongside them sit base64, the UTF-16 family, the comparisons lt, lte, gt and gte, and fieldref for comparing two fields of one event. Version 2.1.0 added neq and a set of time-component modifiers.

cidr matches an IP field against a network range rather than a literal address, so SourceIp|cidr: '10.0.0.0/8' does what you would hope. If you write those ranges by hand, our CIDR calculator confirms the boundaries before a typo becomes a silent gap.

Misspelling a modifier is the classic invisible bug. |containz or |startwith is not a field name any log will have, so depending on the backend the rule either fails to convert or converts into something that can never match.

The condition line#

The condition decides what the rule means. It combines search identifiers with logical operators whose precedence the specification fixes, from least to most binding: or, and, not, the x of patterns, then brackets.

Beyond plain boolean logic there are two quantified forms. 1 of selection_* and all of selection_* match identifiers by wildcard, which keeps a rule with a dozen variants readable. And them means every search identifier whose name does not start with an underscore, so 1 of them is a real expression rather than a figure of speech.

The shape you will see most is selection and not filter: match the behaviour, then subtract the known-benign cases. Worth adopting even when the filter is empty, since tuning later then means adding to the filter rather than editing logic you already reasoned about once.

Metadata that is not decoration#

Several optional fields take a fixed set of values. Anything outside it makes the rule invalid rather than merely unusual.

  • id must be a randomly generated version-4 UUID. It is the rule's durable handle: correlations and filters reference rules by this value, so renaming a rule breaks nothing and changing its id breaks everything downstream.
  • status is one of stable, test, experimental, deprecated or unsupported. Nothing else.
  • level is one of informational, low, medium, high or critical. There is no severity key in Sigma, and writing one produces a rule that loads perfectly and simply has no level.
  • date and modified are ISO 8601 in YYYY-MM-DD form.
  • tags are lowercase and namespaced with dots. In practice most are MITRE ATT&CK references such as attack.t1053.005.
  • related points at another rule by id, typed derived, obsolete, merged, renamed or similar.

The field that earns its keep most often is falsepositives: a plain list of what else legitimately produces this pattern, and the difference between an analyst spending two minutes on an alert and spending forty. Our write-up of impossible travel alerts is largely a catalogue of that kind of benign cause.

From rule to query: backends and pipelines#

Conversion is handled by pySigma, the library, and sigma-cli, the command-line tool built on it. A backend targets one platform and emits its query language. A pipeline maps the generic field names in a rule onto whatever your own logs call them. Most common platforms ship with a pipeline, so writing one is unusual.

That division explains the most confusing outcome in Sigma work: a rule converts without complaint, the query runs without error, and it returns nothing, forever. That is almost never a broken rule. It is a field name the pipeline did not map, so the query asks about a field your data does not have. Validating the rule and validating the mapping are separate jobs.

What changed in Sigma 2#

Rules written for version 1 mostly still read correctly, but a few things moved and older tutorials have not caught up. Dates changed from YYYY/MM/DD to ISO YYYY-MM-DD, the most frequent validation failure on an inherited rule set. Aggregation expressions once appended to the condition after a pipe character were taken out of the rule format and given a specification of their own, alongside filters.

The specification is now split into documents: rules, correlations and filters, with appendices covering modifiers, tags and taxonomy. Its authors place it in the public domain, which is why third-party tooling can implement it without a licensing conversation.

Both new documents are worth knowing exist. A correlation rule references other rules by id and asks a question across their matches, in one of seven types: event_count, value_count, temporal, temporal_ordered, value_sum, value_avg and value_percentile. Ten failed logons is event_count; ten across ten different accounts is value_count. A filter names the rule ids it applies to and suppresses their matches, so one exception for a noisy service account can cover thirty rules without editing any.

Checking a rule before you ship it#

Our Sigma rule linter checks a pasted rule against the 2.1.0 specification, separating errors (specification violations) from warnings (shapes that load but usually indicate a mistake). It verifies the required fields, the permitted status and level values, the UUID and date formats, every modifier against the specification's list, and the condition wiring in both directions: naming a selection that does not exist is an error, and a selection the condition never uses is a warning, usually a filter someone forgot to subtract. Unknown top-level keys are warnings, which is what catches severity.

It is narrow on purpose, and the limits matter as much as the checks. It does not convert rules to any backend, judge whether your selection catches the technique, or validate field names against a product's log schema. It also reads only the slice of YAML that Sigma rules use: meeting an anchor, a flow mapping or tab indentation, it stops and names the construct and the line rather than guessing, because a confident wrong verdict is worse than an admitted limit.

Once a rule is live and producing hits, the work moves to what it surfaces. Our log IP triage ranks the source addresses out of a pasted log, IOC enrichment takes the indicators further, and the rest sits under SOC & IR.

Try it

Lint a Sigma rule

Paste a rule and check it against the 2.1.0 specification: required fields, permitted values, modifier spelling, and condition wiring. Runs in your browser tab.

Check a rule →