SIEM and security operations · Beginner

Detection as code with Sigma

4 min read

Why it matters

Detection engineering, writing and maintaining the rules a SIEM runs, is a growing part of SOC work. Treating detections as code (versioned, reviewed, tested, portable between SIEMs) is how mature teams keep them accurate. Sigma is the open standard for this.

The concept

  • A Sigma rule is YAML: a logsource (which logs), a detection with named selections and a condition, and metadata such as level and status. Converters ("backends") turn it into Splunk SPL, Microsoft Sentinel KQL, Elastic queries or SQL.

  • Single-event rules match one log line, such as "a password login was accepted".

  • Correlations combine events: many failures followed by a success from the same source; activity from two countries within an hour. They catch attacks that single events miss.

  • Test detections against known-bad and known-good data before deploying, and measure how often they fire.

  • Map to MITRE ATT&CK so you know which attacker techniques you can and cannot see.

Worked example

The Sigma rule:

ssh_success_after_bruteforce.yml
title: SSH password login accepted
id: 6f0b2a55-4c1e-4a4e-9d3b-2f6b8b1d9c01
status: experimental
description: A password (not key) login succeeded. On servers that should only allow keys, this is suspicious by itself.
logsource:
  product: linux
  service: sshd
detection:
  selection:
    event: accepted
    method: password
  condition: selection
fields: [ts, user, src_ip]
level: medium

Parse the log into a table, convert the rule with pySigma's SQLite backend, and run it next to a correlation:

detect.py
import re
import sqlite3
from datetime import datetime

from sigma.backends.sqlite import sqliteBackend
from sigma.collection import SigmaCollection

PATTERN = re.compile(
    r"^(?P<ts>\w{3} \d{2} [\d:]{8}) (?P<host>\S+) sshd\[\d+\]: "
    r"(?:(?P<result>Accepted|Failed) (?P<method>password|publickey) for (?P<user>\S+)|Invalid user (?P<invalid>\S+)) "
    r"from (?P<src_ip>[\d.]+)"
)

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE logs (ts TEXT, host TEXT, event TEXT, method TEXT, user TEXT, src_ip TEXT)")
for line in open("auth.log"):
    m = PATTERN.match(line)
    if m:
        event = m["result"].lower() if m["result"] else "invalid_user"
        db.execute("INSERT INTO logs VALUES (?,?,?,?,?,?)",
                   (datetime.strptime("2026 " + m["ts"], "%Y %b %d %H:%M:%S").isoformat(sep=" "), m["host"], event, m["method"], m["user"] or m["invalid"], m["src_ip"]))
print("events parsed:", db.execute("SELECT COUNT(*) FROM logs").fetchone()[0])

# 1. A Sigma rule, converted to SQL by the SQLite backend.
rule = SigmaCollection.from_yaml(open("ssh_success_after_bruteforce.yml").read())
query = sqliteBackend().convert(rule)[0].replace("<TABLE_NAME>", "logs")
print("Sigma -> SQL:", query)
for row in db.execute(query):
    print("  alert:", row[0], row[4], row[5])

# 2. A correlation a single-event rule cannot express: many failures, then a success, same source.
CORRELATION = """
WITH failures AS (
  SELECT src_ip, user, COUNT(*) AS failed, MIN(ts) AS first_failure, MAX(ts) AS last_failure
  FROM logs WHERE event = 'failed' GROUP BY src_ip, user HAVING COUNT(*) >= 10
)
SELECT f.src_ip, f.user, f.failed, f.first_failure, s.ts AS success_at
FROM failures f JOIN logs s
  ON s.src_ip = f.src_ip AND s.user = f.user AND s.event = 'accepted' AND s.ts >= f.last_failure
"""
for row in db.execute(CORRELATION):
    print("  HIGH: {} failures for '{}' from {} starting {}, then success at {}".format(row[2], row[1], row[0], row[3], row[4]))

print("top sources of failed or invalid logins:")
for row in db.execute("SELECT src_ip, COUNT(*) FROM logs WHERE event IN ('failed','invalid_user') GROUP BY src_ip ORDER BY 2 DESC LIMIT 3"):
    print("  ", row)
shellsession
$ pip install pysigma pysigma-backend-sqlite
$ python3 detect.py
events parsed: 205
Sigma -> SQL: SELECT * FROM logs WHERE event='accepted' AND method='password'
  alert: 2026-09-29 02:47:02 deploy 198.51.100.23
  HIGH: 35 failures for 'deploy' from 198.51.100.23 starting 2026-09-29 02:40:00, then success at 2026-09-29 02:47:02
top sources of failed or invalid logins:
   ('203.0.113.50', 40)
   ('198.51.100.23', 35)
   ('10.20.1.22', 4)

The Sigma rule was converted to SQL without writing any SQL by hand; the same file would convert to a Splunk or Sentinel query. It raises one alert, because this server's staff always use keys. The correlation adds what a single-event rule cannot: the success came right after 35 failures from the same address against the same account, which justifies a high severity. Tested versions of both used pySigma 1.5 and the SQLite backend 2.0.

Common mistakes

  • Rules nobody tests. A typo in a field name makes a rule silently never fire.

  • Rules without context. Write a description, a severity and triage steps in the rule itself.

  • Thresholds copied from blogs. Tune on your own data: how many failures does a normal day contain?

  • Only single-event rules. Many real attacks only show up as sequences.

Interview angle

"How would you detect a brute-force attack that succeeded?" (a correlation of failures followed by a success, by source and account, within a window). "How do you keep false positives down?" (baseline normal behaviour, allow-lists for known sources, tuning with data, and severity based on context). "What is Sigma and why use it?"

Practice task

  1. Write a Sigma rule for "new user account created" (the useradd line) and a matching test.

  2. Change the correlation to require the failures within 10 minutes of the success, and check it still fires.

Further reading