A detection-engineering pipeline that treats Sigma rules like code. Every rule is linted (pySigma validators plus project checks), unit-tested against synthetic events, regression-tested against 113 public recordings of ATT&CK techniques (OTRF Security-Datasets: 774,034 Windows events), converted to Splunk SPL, Elastic and Microsoft KQL, and published with an ATT&CK Navigator coverage layer. The data are pre-recorded public logs: nothing is simulated or executed here.
The point of the project is the feedback loop. The first evaluation of the 41 hand-written rules found two bug classes that neither pySigma nor my own linter had caught, and that a code review would most likely miss. Every off-target alert was then reviewed and false positives were filtered, with the before/after numbers below.
| First pass (rules as written) | After fixes and filters | |
|---|---|---|
| Attack recordings detected by a rule of their technique | 75 / 97 (77.3%) | 70 / 97 (72.2%) |
| ... strict: no credit between sibling sub-techniques | 73 / 97 (75.3%) | 68 / 97 (70.1%) |
| In-scope recordings with an alert from any rule | 89 / 97 | 76 / 97 |
| Alerts on the 113 recordings | 499 | 309 |
| ... on the labelled technique | 274 | 243 |
| ... off-target but real attack activity (reviewed) | 72 | 66 |
| ... false positives (reviewed as benign) | 153 (30.7%) | 0 (in-sample) |
| Off-target alert signatures reviewed | 51 (29 benign, 22 attack) | 21 (all attack) |
Detected: a rule sharing an ATT&CK technique or its parent with the recording's label fired on it, so a T1003.002 rule firing on a T1003.001 recording counts. Strict: the same without that credit between two different sub-techniques of one parent; it removes the same two recordings in both passes (a SAM-dump recording caught only by the LSASS-access rule, a DCOM recording caught only by the admin-share rule).
Numbers come from reports/metrics.json and
reports/first_pass/metrics.json; the full breakdown (every
miss, every signature with its verdict, every rule) is in reports/REPORT.md.
What this shows
- Rule bugs are invisible until rules meet data. Four rules repeated a key such as
CommandLine|containsinside one selection. That is valid YAML, so PyYAML silently kept the last key, the first condition vanished, and pySigma parsed the rule without complaint: the SAM-hive rule fired onreg.execommand lines that merely touched a key underHKLM\SYSTEM. A fifth rule matchedShareName|contains: 'C$', which is also true ofIPC$, the named-pipe share every RPC client uses: 45 alerts.dac lintnow rejects duplicate keys, and each bug has a regression test. Data does not catch everything either: a later code review found that the CMSTP rule's value list['/s', '.inf']meant OR where its description says AND (its unit test had been written to the code). It changed no result here and is logged as a bug fix. - The first-pass detection rate was inflated. Five of its 75 detections (remote service
control over SMB) came only from the
IPC$bug: right technique, wrong reason. After the fix they are honest misses, which is why the rate drops from 77.3% to 72.2% while precision improves. - A third of first-pass alerts were noise. The 153 false positives were Windows and OneDrive
maintaining their scheduled tasks (70),
IPC$(45), the Azure guest agent, boot-time handles and WMI queries opening LSASS (13), a monitoring agent's scripts (9), interactive shells (6), Windows network diagnostics running a script andreg.exe(4), domain-controller replication (3) and Defender or Teams registering autostart entries (3). Apart from the two bugs, each source was filtered narrowly (writer path and value name, computer account and built-in task path, task name and the program it runs, ...). The 0 after tuning is in-sample: the filters were written from these recordings, so it is not a false-positive rate for any other environment. - Off-target is not the same as false. 66 alerts fire on recordings labelled with another
technique but are real attack activity. Empire's stager is a PowerShell download cradle,
visible in the 14 Empire recordings whose script-block logs captured it; a DLL-hijack scenario
stages its payload over
C$; a "WMI execution" recording actually creates a WMI event subscription. Counting them as false positives would punish correct rules for the coarseness of dataset-level labels. - Misses are explained, not hidden. Of the 27 misses, 3 recordings lack the event types the
expected rules read (no process-creation or script-block events), and 24 have them but use a
mechanism no rule covers, for example API-based credential theft (Mimikatz over LSARPC/SAMR,
Rubeus), DCOM through Office without a child process, or remote service control seen only as
RPC on the
svcctlpipe.
flowchart LR
R["rules/*.yml<br/>41 Sigma rules"] --> L["dac lint<br/>pySigma validators<br/>+ project checks"]
L --> T["dac test<br/>116 synthetic cases"]
T --> E["dac evaluate<br/>113 recordings, 774k events"]
E --> V{"off-target<br/>alert?"}
V -- yes --> S["signature + analyst verdict<br/>reviews/off_target.yaml"]
V -- no --> M[("metrics.json<br/>REPORT.md, figures")]
S --> M
R --> C["dac convert<br/>SPL, Lucene, ES|QL, KQL"]
M --> N["dac coverage<br/>ATT&CK Navigator layer"]
- Engine (
engine.py). pySigma parses each rule into a condition tree with value modifiers already applied; the engine compiles that tree once into a Python predicate and runs it on the flat JSON events. It supports the Sigma features the rules use and a few more: wildcards,contains/startswith/endswith,all,cased,re(withi,m,s),windash, thebase64family,cidr, numeric comparisons,exists,fieldref, null values, keyword searches,1 of/all of,notand condition lists. Each feature has a test. - Log sources (
logsource.py). Generic categories map to concrete(channel, event id)pairs (process_creationis Sysmon 1 or Security 4688,registry_setis Sysmon 13,ps_scriptis PowerShell 4104...);servicelog sources map to a channel and the rule selects theEventID. Field names follow the Sigma Windows taxonomy, with aliases so that a rule written for Sysmon also runs on native 4688 auditing. - Lint (
lint.py). The offline pySigma validators (the two that download ATT&CK and D3FEND data at runtime are skipped) plus: a strict YAML loader that rejects duplicate keys; unique ids, titles and file names; full metadata (author,date,falsepositives,level...); references only on attack.mitre.org; ATT&CK technique tags that exist in a vendored ATT&CK table (attack.py) plus a tactic tag that belongs to the technique; the file in the folder of its tactic; a log source the engine can run. - Alert signatures (
signatures.py). Off-target alerts are grouped by rule and a few normalised fields: image names, registry keys with SIDs and GUIDs masked, account type, the first command-line argument. Command lines and script text are never part of a signature, so one reviewed verdict covers every alert of the same kind.
Take registry_hive_dump (T1003.002/.004):
- Lint. In the first pass it had two
CommandLine|containskeys in one selection. Todaydac lintstops it:duplicate key 'CommandLine|contains' (line 27): a selection was dropped. - Unit test.
tests/rule_cases/registry_hive_dump.ymlholds 7 synthetic events: saves of the SAM, SECURITY and SYSTEM hives must match; areg addunderHKLM\SYSTEM(the duplicate-key regression) and an export of a SYSTEM subkey must not. Run against the first-pass rule, 2 of them fail. - Regression test.
dac evaluateruns it on all recordings: it detects 3 of the 6 recordings labelled with its techniques and raises 2 off-target alerts, both reviewed as attack activity (the boot-key extraction inside an unlabelled NTDS-dump recording). - Review. Its first-pass signatures (
reg.exe add,export,query) are inreviews/off_target.yamlwith verdicts; the fix is logged inreviews/tuning_log.yamland visible inreports/first_pass/rules.diff. - Convert and publish.
dac convertemits SPL, Lucene, ES|QL and KQL for it, anddac coveragemarks T1003.002 as validated in the Navigator layer.
dac convert uses the pySigma backends (Splunk splunk_windows, Elastic ecs_windows, Kusto
with the Defender XDR tables first and the Azure Monitor / Sentinel SecurityEvent tables as a
fallback). A rule no pipeline can map is recorded with the backend's reason instead of failing the
run. Coverage: SPL 41/41, Lucene 41/41, ES|QL 41/41, KQL 33/41; the 8 KQL gaps use telemetry
the stock Microsoft pipelines have no table for (Sysmon process access and remote threads,
PowerShell script blocks, WMI events, System 7045, scheduled-task audit events). CI publishes the
queries as an artifact. Example, the event-log service being disabled:
Splunk TargetObject="*\\Services\\EventLog\\Start" Details="DWORD (0x00000004)"
Lucene registry.path:*\\Services\\EventLog\\Start AND winlog.event_data.Details:"DWORD \(0x00000004\)"
KQL DeviceRegistryEvents
| where RegistryKey endswith "\\Services\\EventLog\\Start" and RegistryValueData =~ "DWORD (0x00000004)"
The KQL line also shows the limit of syntactic conversion: Defender stores the value as 4, not
in Sysmon's DWORD (0x00000004) format, so a converted rule still needs testing on the target
platform.
- Data. 114 Windows host recordings from OTRF Security-Datasets,
pinned at commit
d9d40ef, each checked against its git SHA-1. One recording is shipped twice under two tactics with byte-identical events and is scored once (113). 7 carry no usable ATT&CK label (6 are referenced by no metadata file, one of them because its metadata links a file name absent at that commit; 1 maps only to theT0000placeholder); they are scanned and their alerts reviewed, but they never count as detections. - Expected detections. A recording is in scope when at least one rule shares an ATT&CK technique with its label, sub-techniques counting under their parent (a T1003.002 rule firing on a T1003.001 recording counts; the strict count drops that credit between siblings). 97 of the 106 labelled recordings are in scope.
- Detected means at least one of those rules fired on at least one event of the recording. A miss is explained as telemetry absent (none of the expected rules' event types is in the recording) or no rule matched.
- Alerts are matching events. A process start logged both by Sysmon (event 1) and by Security auditing (4688) counts as two alerts, as it would in a SIEM that ingests both.
- Off-target alerts (a rule firing on a recording not labelled with its technique) are
grouped into signatures and each signature gets a verdict,
benignorattack, with a one-line rationale.dac evaluateflags any unreviewed signature. - Tuning rules. Only bug fixes and filters for activity reviewed as benign were allowed; no
rule was added or widened after the first evaluation. The first pass can be rebuilt and
re-scored with
make first-pass.
- Dataset-level labels. A label names the technique a recording was made for, not every technique it contains. Multi-step scenarios make "off-target" alerts that are true attack activity; the review separates them, but it is my judgement, recorded and open to challenge.
- Lab recordings, little benign background. Each recording covers minutes on a few lab hosts. The first-pass false-positive count shows the kind of noise these rules raise; it is not an estimate of alert volume on a production network, and the post-tuning zero is in-sample.
- One recording per technique, often. Most techniques have one or two recordings, all produced with a handful of frameworks (Empire, Covenant, Metasploit, Atomic tests). A rule that detects them can still miss another implementation.
- Tuned and scored on the same data. The filters came from reviewing these recordings, so the after-tuning numbers are optimistic; the first pass is the unbiased one.
- Engine, not a SIEM. The matcher implements Sigma semantics over flat JSON. Converted queries are syntactically valid backend output but were not run on Splunk, Elastic or Sentinel.
- Coverage is partial. 10 techniques exercised by the recordings have no rule (for example
T1550.002, T1574.001, T1123), and 3 rules have no validated detection on their own technique
(
rubeus_kerberos_theft,network_share_session_discovery,powershell_download_cradle, the last of which does fire in 14 Empire scenarios labelled with other techniques).
The protocol freezes rule logic after the first pass (bug fixes and benign filters only), and the recordings hold too little benign activity to measure the noise below, so these are v2 work, to be evaluated on new data rather than tuned in-sample:
dcom_lateral_executiontreatsexplorer.exeas a DCOM server (the ShellWindows route). On a workstation explorer.exe startscmd.exe /c, PowerShell andrundll32.exeall day; only argument-less shells are filtered, so the rule needs a DCOM-specific signal before production.create_remote_thread_injectionalerts at levelhighon any Sysmon 8 event outside five system processes, and filters those by image name only (\csrss.exe), so a binary with the same name in another folder is excluded too. It needs full-path filters and a lower level.account_discovery_netmatchescontains 'user', which is also true ofnet use \\host\share /user:...(mapping a share with credentials) and ofnet user /add(account creation, seen in the review).eventlog_service_disabled_powershellalso covers Defender services (WinDefend,SecurityHealthService), which is T1562.001 and not tagged; itsnetprofmtarget only matches the preparatory step these recordings take before stopping the Event Log.
| Tactic | Rule | ATT&CK | Telemetry | Own-technique recordings detected |
|---|---|---|---|---|
| Credential access | Active Directory Replication From Non-DC (DCSync) | T1003.006 | Security 4662 | 2 / 2 |
| Credential access | LSASS Memory Dump via comsvcs.dll MiniDump | T1003.001 | Sysmon 1 / Security 4688 | 1 / 5 |
| Credential access | LSASS Process Memory Accessed With Dump-Capable Rights | T1003.001 | Sysmon 10 | 5 / 5 |
| Credential access | Active Directory NTDS.dit Database Extraction | T1003.003 | Sysmon 1 / Security 4688 | 1 / 4 |
| Credential access | SAM/SECURITY/SYSTEM Registry Hive Dump | T1003.002, T1003.004 | Sysmon 1 / Security 4688 | 3 / 6 |
| Credential access | Kerberos Ticket Theft Tooling in PowerShell | T1558.003 | PowerShell 4104 | 0 / 1 |
| Defense evasion | Audit Policy Disabled via auditpol | T1562.002 | Sysmon 1 / Security 4688 | 1 / 17 |
| Defense evasion | File Download via BITS Job | T1197 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | CMSTP Silent INF Proxy Execution | T1218.003 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | Compiled HTML Help Executing a CHM Payload | T1218.001 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | Remote Thread Created in Another Process | T1055 | Sysmon 8 | 4 / 6 |
| Defense evasion | Process Command-Line Audit Logging Disabled | T1562.002 | Sysmon 13 | 2 / 17 |
| Defense evasion | Event Log Service Disabled via Start-Type Registry | T1562.002 | Sysmon 13 | 3 / 17 |
| Defense evasion | Security Event Log Cleared or Relocated via wevtutil | T1070.001, T1562.002 | Sysmon 1 / Security 4688 | 1 / 17 |
| Defense evasion | Logging Service Disabled via PowerShell Service Cmdlet | T1562.002 | PowerShell 4104 | 5 / 17 |
| Defense evasion | InstallUtil Uninstall Proxy Execution | T1218.004 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | Mavinject Running-Process DLL Injection | T1055 | Sysmon 1 / Security 4688 | 1 / 6 |
| Defense evasion | Mshta Executing Script or Remote Content | T1218.005 | Sysmon 1 / Security 4688 | 3 / 3 |
| Defense evasion | Windows Firewall Rule Added via Netsh | T1562.004 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | DLL Executed via Register-CimProvider | T1218 | Sysmon 1 / Security 4688 | 1 / 10 |
| Defense evasion | Regsvr32 Scriptlet or Remote COM Registration | T1218.010 | Sysmon 1 / Security 4688 | 1 / 1 |
| Defense evasion | Event Logging Disabled via MiniNt Registry Key | T1562.002 | Sysmon 12-14 | 6 / 17 |
| Defense evasion | WMIC Executing a Remote or Local XSL Script | T1220 | Sysmon 1 / Security 4688 | 1 / 1 |
| Persistence | Logon Script Registered via UserInitMprLogonScript | T1037.001 | Sysmon 13 | 1 / 1 |
| Persistence | Autorun Registry Run Key Set | T1547.001 | Sysmon 13 | 2 / 2 |
| Persistence | Scheduled Task Created or Updated | T1053.005 | Security 4698/4702 | 4 / 4 |
| Persistence | Scheduled Task Created via schtasks | T1053.005 | Sysmon 1 / Security 4688 | 3 / 4 |
| Persistence | Web Server Process Spawning a Command Shell | T1505.003 | Sysmon 1 / Security 4688 | 1 / 1 |
| Persistence | WMI Permanent Event Subscription Created | T1546.003 | Sysmon 19-21 | 1 / 1 |
| Privilege escalation | Existing Service Binary Path Modified via sc | T1543.003 | Sysmon 1 / Security 4688 | 1 / 1 |
| Privilege escalation | UAC Bypass via ms-settings Shell Open Command | T1548.002 | Sysmon 12-14 | 1 / 1 |
| Lateral movement | Access to a Hidden Administrative Share | T1021.002 | Security 5145 | 1 / 7 |
| Lateral movement | Command Shell Spawned by a DCOM Server | T1021.003 | Sysmon 1 / Security 4688 | 1 / 4 |
| Lateral movement | Service Installed to Run a Command Interpreter | T1021.002 | System 7045 | 1 / 7 |
| Lateral movement | Process Launched by WinRM Host Process | T1021.006 | Sysmon 1 / Security 4688 | 2 / 3 |
| Discovery | Account Discovery via net user | T1087.001, T1087.002 | Sysmon 1 / Security 4688 | 2 / 2 |
| Discovery | Local or Domain Group Membership Discovery via net | T1069.001, T1069.002 | Sysmon 1 / Security 4688 | 2 / 4 |
| Discovery | Network Share and Session Discovery via net | T1049, T1135 | Sysmon 1 / Security 4688 | 0 / 2 |
| Execution | PowerShell Download Cradle in Script Block | T1059.001 | PowerShell 4104 | 0 / 1 |
| Execution | Process Launched by WMI Provider Host | T1047 | Sysmon 1 / Security 4688 | 3 / 6 |
| Execution | Script Host Executing a Script File | T1059.005 | Sysmon 1 / Security 4688 | 1 / 1 |
The last column counts in-scope recordings labelled with one of the rule's own techniques. Several rules share a technique (17 recordings exercise T1562.002, covered by 6 rules that each target one mechanism), so a low ratio on one rule is not a miss of the technique.
The ATT&CK Navigator layer is reports/attack_navigator_layer.json
(load it in the Navigator): 36 techniques with a
validated rule, 8 with a rule not validated on this data, 10 exercised by recordings but not
covered.
git clone https://github.com/hugo-kte/detection-as-code.git && cd detection-as-code
make install # .venv with the dependency versions pinned in constraints.txt
make rules-lint # pySigma validators + project checks
make test # 199 tests, no dataset needed
make convert # SPL / Lucene / ES|QL / KQL -> reports/queries/
make download # 214 pinned files (~61 MB) from OTRF Security-Datasets, SHA-1 verified
make evaluate # detection matrix, metrics, report, figures -> reports/
make coverage # ATT&CK Navigator layer
make first-pass # rebuild and re-score the rules as first writtendac lint [--rules DIR]
dac test [--rules DIR] [--cases DIR]
dac evaluate [--data DIR] [--rules DIR] [--reviews FILE] [--out DIR] [--jobs N]
dac convert [-o DIR] [--target splunk|lucene|esql|kql ...]
dac coverage [--metrics reports/metrics.json] [-o FILE]
dac download [-o DIR]
Adding a rule: write rules/<tactic>/<name>.yml, add tests/rule_cases/<name>.yml with at
least one matching and one non-matching event, then make rules-lint test evaluate and give a
verdict to any new off-target signature in reviews/off_target.yaml.
rules/<tactic>/*.yml 41 Sigma rules
tests/rule_cases/*.yml 116 synthetic must-match / must-not-match events, one file per rule
reviews/off_target.yaml analyst verdict for every off-target alert signature
reviews/tuning_log.yaml every change made after the first evaluation, and why
src/dac/
├── engine.py compiles pySigma condition trees into event predicates
├── logsource.py Sigma log source -> (channel, event id), field aliases
├── lint.py pySigma validators + project checks (strict YAML, ATT&CK tags...)
├── ruletest.py runner for tests/rule_cases
├── datasets.py pinned manifest, verified download, catalogue, loader
├── evaluate.py parallel scan, expected detections, misses, off-target, metrics
├── signatures.py, review.py alert signatures and analyst verdicts
├── convert.py pySigma backends with fallbacks and graceful failures
├── coverage.py ATT&CK Navigator layer
├── attack.py vendored slice of ATT&CK (techniques, tactics)
└── report.py, plots.py, cli.py
configs/security_datasets.json pinned file list with git SHA-1s
constraints.txt pinned dependency versions for CI and make install (make lock)
reports/ metrics.json, REPORT.md, detection_matrix.csv, figures/,
attack_navigator_layer.json, first_pass/ (metrics, report, rules.diff)
Recordings: OTRF Security-Datasets (formerly Mordor),
MIT License, by Roberto Rodriguez (@Cyb3rWard0g), Jose Rodriguez
(@Cyb3rPandaH) and contributors, pinned at commit
d9d40ef123d2c87d5d3df28c96bcab4f0faccc87. The data are downloaded by make download and never
committed; only the labels' technique IDs are used, and the scenario descriptions in the upstream
metadata are not copied here. Technique names and IDs are from MITRE ATT&CK® Enterprise v16.
Rules and test events in this repository describe what to detect, not how to perform an attack.
Code and rules: MIT.
@misc{otrf_security_datasets,
author = {Rodriguez, Roberto and Rodriguez, Jose},
title = {Security Datasets},
year = {2019},
howpublished = {\url{https://github.com/OTRF/Security-Datasets}},
note = {Open Threat Research Forge, commit d9d40ef}
}Part of a cybersecurity & AI engineering portfolio: FlowGuard (network IDS) · Detection-as-code (Sigma rules) · Insider-threat UEBA · AlertLens (SOC triage) · PromptWall (LLM firewall) · MLSeal (secure MLOps) · Gleaner (CTI to STIX).


