Contents
The most expensive thing an incident-response engagement can produce is a finding the client cannot detect themselves next time. Every engagement ends with the same job — turn what we did manually into automation the SOC can run forever.
Sigma is the format I use for that handoff.
Anatomy of a Sigma rule
The format is YAML, and the schema is small enough to memorise:
title: Suspicious cPanel API token creation
id: 8a4e1b2c-7f30-4d6e-9c1a-2b3f5a6c8d9e
status: experimental
description: |
Detects creation of cPanel API tokens with admin-equivalent scopes,
which was the persistence mechanism used in the click-fix campaign
that abused a zero-day across 60+ Cloudflare-fronted subdomains.
references:
- https://8bytes.in/blog/click-fix-zero-day
author: 8bytes
date: 2025-12-15
logsource:
product: cpanel
service: api
detection:
selection:
event_type: token.create
scopes|contains:
- 'admin'
- 'reseller'
filter_known_admins:
user|startswith: 'svc_'
condition: selection and not filter_known_admins
level: high
tags:
- attack.persistence
- attack.t1098.001
Six required keys, two optional, one detection block. That is the entire format.
The event we matched against
The original log entry that triggered the rule — lightly trimmed for readability:
{
"eventTime": "2026-04-12T03:14:22Z",
"eventName": "CreateToken",
"eventSource": "cpanel.api",
"userIdentity": {
"type": "User",
"userName": "marketing-intern-3"
},
"requestParameters": {
"tokenName": "marketing-automation",
"scopes": ["admin", "ftp", "files"]
},
"sourceIPAddress": "185.247.137.42",
"userAgent": "curl/7.88.1"
}
The intern account had no business creating an admin-scoped token. That single field combination — non-service-account user + admin scope — is the heart of the detection.
Testing the rule before shipping it
The sigma CLI compiles a rule against the target backend and replays events through it. Run this as a pre-commit hook and you cannot ship a rule that silently fails to match:
# Compile to your SIEM's query language
sigma convert -t splunk rules/cpanel-admin-token.yml
# Replay historical events against the rule
sigma test \
--rule rules/cpanel-admin-token.yml \
--events fixtures/cpanel-tokens.json \
--expect-match 'event_id == "evt_abc123"'
# Diff against the previous version to catch regressions
sigma diff rules/cpanel-admin-token.yml HEAD~1
A small Python helper for selector hunting
Half my Sigma authoring is staring at log fields and figuring out which ones are stable. This snippet enumerates distinct values for every top-level field in a JSONL log file — enough to pick durable selectors quickly:
import json
from collections import defaultdict
def field_cardinality(path: str) -> dict[str, set[str]]:
"""Return distinct values seen for every top-level field in a JSONL file."""
fields: dict[str, set[str]] = defaultdict(set)
with open(path) as f:
for line in f:
event = json.loads(line)
for key, value in event.items():
if isinstance(value, (str, int, bool)):
fields[key].add(str(value))
return {k: v for k, v in fields.items() if len(v) < 50}
if __name__ == "__main__":
import sys
for field, values in field_cardinality(sys.argv[1]).items():
print(f"{field:30s} {len(values):4d} {sorted(list(values))[:5]}")
Fields with low cardinality (< 50 distinct values) are the ones I want in my selectors. Anything that varies per-event — timestamps, request IDs, source IPs — gets filtered out before I see it.
The full pipeline
A mature detection-as-code workflow ends up looking like this:
.
├── rules/
│ ├── cpanel-admin-token.yml
│ ├── aws-iam-privesc.yml
│ └── graphql-introspection-abuse.yml
├── fixtures/
│ ├── cpanel-tokens.json
│ └── cloudtrail-iam.json
├── tests/
│ └── run_all.sh
└── .github/workflows/
└── sigma-ci.yml
CI runs sigma test on every pull request. The SIEM ingests merged rules nightly. Every IR engagement we close adds at least one rule that prevents the same thing happening twice.
That last sentence is the whole pitch: the value of an engagement compounds when the detections survive the engagement.
Tags