Lesson 3 of 5 · 30 min read

Testing Detection Rules — Unit, Integration, and Regression

A detection rule without tests is a claim about behavior that no one has verified. Testing gives you two guarantees: the rule fires for real attacks, and the rule doesn’t fire for known-legitimate events. Without both guarantees, you’re deploying hope, not security.


The Three Testing Levels

Level 1: Unit Test — Does the rule convert?

# Validate Sigma YAML syntax and semantics
sigma check rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml

# Output (success):
# [OK] rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml

# Output (failure):
# [WARN] proc_creation_powershell_encoded_cmd.yml: field 'OriginalFileName' not in
#         recommended field list for logsource windows/process_creation
# [ERROR] proc_creation_powershell_encoded_cmd.yml: condition references undefined
#         selection 'selection_encoded' (defined: selection_img, filter_sccm)
# Validate conversion succeeds for all target backends
sigma convert -t splunk -p sysmon \
  rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml
sigma convert -t elasticsearch -p ecs-windows \
  rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml
sigma convert -t microsoft365defender \
  rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml

Conversion failures indicate: field not in pipeline mapping, modifier not supported by backend, or unsupported condition syntax.

Level 2: Integration Test — Does the rule fire for attacks?

Run the converted query against a known-TP dataset:

# test_rule.py (simplified)
import json
import subprocess

def test_rule_fires_on_tp():
    # Load TP fixture
    with open("tests/windows/process_creation/proc_creation_powershell_encoded_cmd-tp.json") as f:
        tp_events = json.load(f)
    
    # Convert rule
    result = subprocess.run(
        ["sigma", "convert", "-t", "test-backend", 
         "rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml"],
        capture_output=True, text=True
    )
    query = result.stdout
    
    # Run query against TP events (using test-backend evaluation)
    matches = evaluate_query(query, tp_events)
    assert len(matches) >= 1, f"Rule should match TP events, got {len(matches)} matches"

def test_rule_excludes_fp():
    # Load FP fixture
    with open("tests/windows/process_creation/proc_creation_powershell_encoded_cmd-fp.json") as f:
        fp_events = json.load(f)
    
    query = convert_rule(...)
    matches = evaluate_query(query, fp_events)
    assert len(matches) == 0, f"Rule should not match FP events, got {len(matches)} matches"

Level 3: Regression Test — Still fires after changes?

After any rule modification (adding exclusion, changing condition), run the TP test again to confirm the modification didn’t accidentally suppress TPs:

# Run the full test suite after modifying a rule
python -m pytest tests/ -v -k "powershell_encoded"

# Output:
# PASSED test_rule_fires_on_tp[proc_creation_powershell_encoded_cmd]
# PASSED test_rule_excludes_fp[proc_creation_powershell_encoded_cmd]

If the TP test starts failing after a rule change — the change is a regression.


Writing TP and FP Fixtures

Where to get TP data

SourceQualityEffort
Atomic Red Team test output (saved from your lab)HighLow — run the test, save the event
EVTX-ATTACK-SAMPLES (sbousseaden GitHub repo)HighLow — pre-built EVTX files
Mordor dataset (Roberto Rodriguez)Very highLow — JSON format, pre-labeled
Synthetic (hand-crafted)MediumMedium — craft the event fields manually

Preferred: Run the Atomic Red Team test in your lab, capture the Sysmon Event from your SIEM as JSON, store it as the TP fixture. This is the highest-fidelity test because it’s actual telemetry from the real attack.

# Capture Sysmon event from Splunk after atomic test
| search index=sysmon EventCode=1 Image="*powershell*" earliest="-5m"
| table _time, EventCode, Image, CommandLine, ParentImage, User
| outputlookup tp_fixture.csv

Writing a synthetic TP fixture

When you don’t have real attack data, craft a minimal fixture with exactly the fields the rule checks:

// proc_creation_powershell_encoded_cmd-tp.json
[
  {
    "EventID": 1,
    "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
    "CommandLine": "powershell.exe -EncodedCommand SGVsbG8gV29ybGQ=",
    "ParentImage": "C:\\Windows\\explorer.exe",
    "User": "CORP\\testuser",
    "ComputerName": "WIN10-TEST"
  },
  {
    "EventID": 1,
    "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
    "CommandLine": "powershell.exe -enc SGVsbG8gV29ybGQ=",
    "ParentImage": "C:\\Windows\\explorer.exe",
    "User": "CORP\\testuser",
    "ComputerName": "WIN10-TEST"
  }
]

Include multiple variants (full flag + abbreviated) to test recall within the fixture.

Writing FP fixtures

FP fixtures should represent the actual known-legitimate events that previously generated FPs:

// proc_creation_powershell_encoded_cmd-fp.json
[
  {
    "EventID": 1,
    "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
    "CommandLine": "powershell.exe -ExecutionPolicy Bypass -NonInteractive -File C:\\SCCM\\deploy.ps1",
    "ParentImage": "C:\\Windows\\CCM\\CcmExec.exe",
    "User": "SYSTEM",
    "ComputerName": "WIN10-TEST"
  }
]

This tests that the filter_sccm exclusion works — the SCCM parent triggers the exclusion and no alert fires.


The sigma-testing Framework

sigma-testing provides a test manifest format that wraps the process above:

# tests/proc_creation_powershell_encoded_cmd.test.yml
title: PowerShell Encoded Command Test Cases
rules:
  - rules/windows/process_creation/proc_creation_powershell_encoded_cmd.yml

tests:
  - title: Encoded PowerShell execution (full flag)
    rule: proc_creation_powershell_encoded_cmd.yml
    result: positive
    event:
      EventID: 1
      Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
      CommandLine: 'powershell.exe -EncodedCommand SGVsbG8='
      ParentImage: 'C:\Windows\explorer.exe'

  - title: Encoded PowerShell execution (abbreviated flag)
    rule: proc_creation_powershell_encoded_cmd.yml
    result: positive
    event:
      CommandLine: 'powershell.exe -enc SGVsbG8='

  - title: Normal PowerShell (should not fire)
    rule: proc_creation_powershell_encoded_cmd.yml
    result: negative
    event:
      CommandLine: 'powershell.exe -Command "Write-Host hello"'

  - title: SCCM PowerShell deployment (filtered)
    rule: proc_creation_powershell_encoded_cmd.yml
    result: negative
    event:
      CommandLine: 'powershell.exe -enc SGVsbG8='
      ParentImage: 'C:\Windows\CCM\CcmExec.exe'

Run with:

sigma test tests/proc_creation_powershell_encoded_cmd.test.yml
sigma-cli— SigmaHQ EVTX-ATTACK-SAMPLES— GitHub Mordor / Security Datasets— OTRF