The Ops Atlas xOps School

Security & Compliance Overlapping synonym Sources verified

DetectionOps

DetectionOps is a rare label for detection engineering run like software delivery: detections are written as code from threat-informed requirements, tested, peer-reviewed, deployed through a pipeline, measured in production, tuned and retired.

Key takeaways

  • "DetectionOps" is a rare label: every documented use found comes from the European Commission's OpenTIDE project. The practice behind it is detection engineering run as detection-as-code; search for those names.
  • Treat every detection as a small product: a requirement, a data dependency, logic in version control, tests that prove it fires and stays quiet, an owner, and a date to review or retire it.
  • A green pipeline proves logic against fixtures, not that a rule fires in production. Check rule runs and live data, and emulate periodically in an authorised lab.
  • An ATT&CK tag is not coverage. Count validated coverage per technique implementation and judge detection quality (robustness and precision), as MITRE's Summiting the Pyramid advises.
  • Every filter buys precision with a blind spot. Record accepted gaps as tests so they stay visible.
  • Frameworks and toolchains move (ATT&CK v19 renamed tactics; pySigma 2.0 outpaced sigma-cli). Pin toolchains and maintain mappings as part of each detection.

DetectionOps is detection engineering run like software delivery. A detection (a query in a SIEM, the platform that collects and searches security logs; a rule in an endpoint detection and response (EDR) tool; a Sigma rule) starts from a written requirement: which threat, on which assets, seen in which data. It lives in version control with tests proving it fires on malicious examples and stays quiet on benign ones, passes review, is deployed through staging by a pipeline, is judged by analysts’ verdicts, and is tuned or retired on purpose. The page serves detection and SOC engineers, purple teams, and leaders who must say what their detections cover.

Question Short answer
Goal A versioned, tested library of detections whose coverage and alert quality are known
Main users Detection engineers, SOC engineers, purple teams; analysts as customers
Inputs Threat intelligence, hunt findings, incident lessons, SOC feedback, asset priorities
Outputs Production detections with tests and documentation, coverage evidence, tuning and retirement records
Prerequisites Working log collection (SecDataOps), a SOC that works alerts (SOC Ops), basic Git
Out of scope Triage, response, intelligence, telemetry pipelines, ML detection
Term status Overlapping synonym for detection engineering and detection-as-code

Detections created by hand in a console cause three problems. Nobody can say what the library covers, because rules carry no requirement or evidence. Changes are invisible: a 2 a.m. exclusion creates an unknown blind spot. And rules decay silently: a parser update renames a field, and the query runs but never matches again.

Palantir’s incident response team described the result in its Alerting and Detection Strategy (ADS) framework: a “lack of rigor” meant “sub-optimal alerts going to production without documentation or peer-review”, which “led to fatigue, alerting apathy, or additional engineering time and resources”[6]. SANS’s summary of its 2025 survey (264 respondents) says “Only 45% of organizations have a way to reliably measure the effectiveness of their detection engineering efforts”[3]. The sponsor’s summary of the 2026 survey (307 practitioners) reports 62% using version control for rules, 58% peer review and 42% CI/CD integration[7].

  • Not the SOC, hunting or incident response. Triage belongs to SOC Ops, intelligence and hunting to ThreatOps, confirmed incidents to ResponseOps.
  • Not log engineering. Onboarding, parsing and retention belong to SecDataOps; this page only names the data dependency.
  • Not machine-learning detection. Anomaly detection, user and entity behaviour analytics (UEBA) and LLM triage are evaluated in AIOps for Security.
  • 2013: behaviour over artefacts. David Bianco’s Pyramid of Pain (revised in 2014, when hash values became its bottom level) ranks indicators up to TTPs (tactics, techniques and procedures), where defenders work “directly on adversary behaviors, not against their tools”[8].
  • 2017: documentation and portable rules. Palantir published the ADS framework[6]; Sigma, “for log files what Snort is for network traffic and YARA is for files”[9], was presented at Hack.lu[10].
  • 2021–2024: maturity models. Kyle Bailey’s Detection Engineering Maturity Matrix (2021) treated detection-as-code as a technology dimension[11]; Elastic’s DEBMM (2024) defined five tiers[4].
  • 2022–2026: the label. The European Commission’s security operations centre put OpenTIDE into operation in September 2022 and released it as open source in 2024[12]. Its maintainers’ 2024 Hack.lu workshop opened with an “Introduction to DetectionOps with OpenTIDE”[13]; a 2026 workshop builds an “Operational DetectionOps Platform” (due 23 October 2026)[14].
  • 2023–2026: measurement matures. CTID’s Summiting the Pyramid (2023; version 5.0 in 2026) defined robustness levels and coverage measures beyond technique tags[15].
Term Meaning on this page Example
Detection (rule, analytic) Logic that turns telemetry into an alert or signal Query for tasks whose action runs from C:\Users\Public\
Query languages KQL (Microsoft Sentinel), SPL (Splunk), ES|QL (Elastic), YARA-L (Google SecOps) —
Sigma; backend; pipeline Open YAML rule format; a backend converts it to a query language; a pipeline maps fields and log sources[16] -t splunk -p sysmon
Detection strategy (ATT&CK) ATT&CK’s approach for detecting a technique through platform-specific analytics[17], with tunable “mutable elements”[18] DET0441 for scheduled tasks
Disposition The SOC’s verdict: true positive (malicious), benign positive (rule worked, activity legitimate), false positive (logic or data wrong), undetermined[19] The ChatApp updater: benign positive
False negative Malicious activity that raised no alert A task created via PowerShell, missed by rule A
Precision and recall Low probability of alerting on benign activity; high probability of detecting malicious events[20]. Formulas: the Atlas’s, see Metrics 6 of 40 alerts true → precision 15%
Robustness How hard a signal is to avoid while still achieving the goal[21] Task-registration events beat a tool’s command line
Implementation A behaviourally distinct way to perform a technique[22] PowerShell, a registry write, schtasks or XML for a scheduled task
Emulation; purple team Reproducing adversary behaviour safely in an authorised lab; attackers and defenders doing it together An Atomic Red Team test
Filter Logic that suppresses known benign matches, always creating a blind spot Exclude one updater

Mental model: a detection is a small product with a contract. For this behaviour, on these assets, using this data, it alerts within this time at roughly this precision, with these gaps accepted. Git stores the contract, tests check it, production measures it, and review dates force renewal or cancellation.

These boundaries follow the flow of security work; they are editorial distinctions, not a standard.

Neighbour What that page owns Handoff with DetectionOps
SecDataOps Onboarding, parsing, normalisation, retention, cost Data gaps go there to be fixed
ThreatOps Intelligence lifecycle, hunting A validated hunt finding becomes a detection request
SOC Ops Triage, tiers, shifts, managed detection and response (MDR) Dispositions and tuning requests flow back
ResponseOps Containment, eradication, evidence Missed or late detections become requests
AIOps for Security ML, UEBA, LLM assistance, autonomy limits ML is evaluated there; rules here
DevSecOps Security in software delivery Pipeline practice; application logs
SecOps Security–IT collaboration Operations announces changes that break detections
Figure 1. Where DetectionOps sits in the flow of security work
requestsmisseseventsdata gapsalerts, runbooksdispositionsincidentsThreatOpsResponseOpsSecDataOpsDetectionOpsSOC Ops
Diagram source (Mermaid)
flowchart TB
  TO["ThreatOps"]
  RO["ResponseOps"]
  SD["SecDataOps"]
  DO["DetectionOps"]
  SOC["SOC Ops"]
  TO -->|"requests"| DO
  RO -->|"misses"| DO
  SD -->|"events"| DO
  DO -->|"data gaps"| SD
  DO -->|"alerts, runbooks"| SOC
  SOC -->|"dispositions"| DO
  SOC -->|"incidents"| RO
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class DO core
  class TO,RO,SD,SOC neutral

Text equivalent: detection requests arrive from ThreatOps (intelligence, hunts) and ResponseOps (missed detections), normalised events from SecDataOps (data gaps go back), alerts with runbooks go to SOC Ops, dispositions return, and confirmed incidents go from SOC Ops to ResponseOps.

No body publishes a DetectionOps manifesto. This charter is the Atlas’s proposal, distilled from the ADS framework, the SANS life cycle, Summiting the Pyramid and the maturity models cited here.

Mission. Detections for the threats that matter here, with known coverage, noise and gaps, kept true as things change.

  1. Threat-informed, asset-aware. Every detection traces to a threat that can reach something you run.
  2. Behaviour over artefacts. Prefer signals an adversary cannot easily avoid; hashes and tool names are short-lived additions[8][21].
  3. Everything as code, everything reviewed. Rules, tests, filters and mappings live in version control; no console-only edits.
  4. Tests are part of the rule. A rule without a positive and a negative test is a draft; accepted blind spots are tests too.
  5. Measure in production. Judge rules by analysts’ dispositions and by emulation results, not by whether they compile.
  6. Every exclusion is a decision with an owner, an expiry and a record of what it hides.
  7. Retire on purpose, with a reason, rather than silently disabling.

Non-goals. DetectionOps does not promise to detect everything, replace prevention, or own the SOC’s queue.

Activity Detection engineer Detection lead SOC ThreatOps SecDataOps Platform owner
Accept and prioritise a request C A C R (intel input) C I
Write rule, tests and strategy R A C C C I
Peer review R (second engineer) A C I C (fields) I
Deploy to production R A I I I C (change window)
Triage alerts, record dispositions I I A/R I I I
Tune or add a filter R A C (evidence) I I I
Fix a data gap C I I I A/R C
Retire a rule R A C C I I

R = responsible, A = accountable, C = consulted, I = informed. Small teams merge roles; each decision keeps one accountable person.

The SANS poster’s life cycle runs Requirements Discovery, Triage, Investigation, Development, Testing, Deployment[23]; CTID stresses that its workflow steps “are not strictly linear”[5]; Sigma’s status marks maturity (experimental, test, stable), replacement (deprecated) or a rule that cannot be used as it stands (unsupported)[24]. The Atlas adds the states needed after deployment.

Figure 2. Lifecycle of one detection, from request to retirement
no data yetdata arriveslow priorityacceptedrule and teststest failsCI and review passvolume as expectednoisy or silentverdicts or missfix mergedreplaced, obsolete or costlyRequestedParkedRejectedBuildingTestingStagedProductionTuningDeprecated
Diagram source (Mermaid)
stateDiagram-v2
  direction TB
  [*] --> Requested
  Requested --> Parked: no data yet
  Parked --> Requested: data arrives
  Requested --> Rejected: low priority
  Requested --> Building: accepted
  Building --> Testing: rule and tests
  Testing --> Building: test fails
  Testing --> Staged: CI and review pass
  Staged --> Production: volume as expected
  Staged --> Building: noisy or silent
  Production --> Tuning: verdicts or miss
  Tuning --> Production: fix merged
  Production --> Deprecated: replaced, obsolete or costly
  Deprecated --> [*]
  Rejected --> [*]

Text equivalent: a request without data is parked until data arrives; a low-priority one is rejected; others are built. Failing tests return it to building; passing CI and review stages it. Expected volume promotes it; noise or silence sends it back. Dispositions or an emulation miss send it to tuning; replaced, obsolete or costly rules are deprecated.

Staging deserves its own discipline. Run new and changed rules where they cannot page anyone (a test tenant, a separate index, alerting off) for about seven days and compare volume and samples with the request. OpenTide stages from the pull request and deploys production on merge[25].

Cadence What happens Output
Per change Pull request with rule, tests, strategy; CI; one reviewer; staged run Merged change, staging evidence
Weekly Backlog triage with the SOC lead; review of the noisiest rules Ranked backlog, tuning tickets
Monthly Health review: silent rules, failing checks, expired filters Fix or retire decisions
Quarterly Coverage review against the threat model and emulation results Coverage report, priorities
On framework change Remap tags after ATT&CK or schema releases; re-pin toolchain Updated mappings, green CI

Change classes (Atlas proposal). Standard follows the full path. Emergency (active campaign) may skip staging, never tests or review, and is re-reviewed within five working days. Filter changes need SOC evidence and an expiry. Align with ITOps change routes where they apply.

The parts are ordinary (repository, CI runner, converter, platform APIs); what makes it DetectionOps is that every arrow is automated or recorded.

Figure 3. Reference architecture for detection-as-code
changes as pull requestsInputs: intel, hunts, incidentlessons, SOC requestsRepository: rules, tests,filters, strategies, mappingsPipeline: lint, unit tests,convert per platformStaging: shadow mode, nopagingProduction SIEM and EDRHealth monitoring: rule runs,data presence, canariesFeedback: dispositions,emulation resultsCoverage store:implementations, evidence,gaps
Diagram source (Mermaid)
flowchart TB
  IN["Inputs: intel, hunts, incident lessons, SOC requests"]
  REPO["Repository: rules, tests, filters, strategies, mappings"]
  CI["Pipeline: lint, unit tests, convert per platform"]
  STG["Staging: shadow mode, no paging"]
  PRD["Production SIEM and EDR"]
  HM["Health monitoring: rule runs, data presence, canaries"]
  FB["Feedback: dispositions, emulation results"]
  COV["Coverage store: implementations, evidence, gaps"]
  IN --> REPO
  REPO --> CI
  CI --> STG
  STG --> PRD
  PRD --> HM
  HM --> FB
  FB --> COV
  FB -.->|"changes as pull requests"| REPO
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class REPO,CI core
  class IN,STG,PRD,HM,FB,COV neutral

Text equivalent: requests feed a repository of rules, tests, filters, strategies and mappings. A pipeline lints, tests and converts them for staging in shadow mode, then production SIEM and EDR. Health checks (rule runs, data, canaries), dispositions and emulation results form the feedback, which fills a coverage store and returns changes as pull requests.

Component Responsibility Design choices and trade-offs
Repository Source of truth for rules, tests, filters, mappings, docs Private (see Quality, security and compliance)
Rule format Sigma for portability, or native KQL, SPL, ES|QL, YARA-L See the judgement call
Tests Positive, negative and known-gap fixtures; regression samples Synthetic is fast; real samples catch parser reality
Pipeline Lint, validate, test, convert, deploy Pin versions; fail on validation issues
Health monitoring Find rules that stop running, seeing data or matching Rule-run logs; data-presence and canary checks per rule
Coverage store Rules mapped to techniques, implementations and assets Evidence from emulation, not tags
Figure 4. From detection request to production: the gates and who passes them
acceptedCI green; no secrets usedapprove; merge to protectedmain7 days; promotion checklistpassedproduction reviewerapproves, not the authordispositions, tuning requestsDetection request: huntfinding, intel or incidentSearch SigmaHQ and vendorcontent firstPull request: rule, tests,strategySecond engineer reviewsStaging: shadow mode, nopagingMaintainer tags release-* onmainProduction: alerts reach theSOC
Diagram source (Mermaid)
flowchart TB
  REQ["Detection request: hunt finding, intel or incident"]
  LIB["Search SigmaHQ and vendor content first"]
  PR["Pull request: rule, tests, strategy"]
  REV["Second engineer reviews"]
  STG["Staging: shadow mode, no paging"]
  TAG["Maintainer tags release-* on main"]
  PRD["Production: alerts reach the SOC"]
  REQ -->|"accepted"| LIB
  LIB --> PR
  PR -->|"CI green; no secrets used"| REV
  REV -->|"approve; merge to protected main"| STG
  STG -->|"7 days; promotion checklist passed"| TAG
  TAG -->|"production reviewer approves, not the author"| PRD
  PRD -.->|"dispositions, tuning requests"| REQ
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class PR,STG,PRD core
  class REV,TAG decision
  class REQ,LIB neutral

Text equivalent: an accepted request starts with a search of existing content. A pull request must pass CI without secrets; a second engineer approves the merge to protected main, which deploys to staging. After seven days and the promotion checklist, a maintainer tags release-* on main and a reviewer other than the author approves production. SOC dispositions start the next change.

Exception paths.

  • No data. Park the request as a data gap for SecDataOps, naming the fields required; never write a rule against data you do not have.
  • Tests or conversion fail. The pull request cannot merge. Conversion failures usually mean an unsupported modifier or unmapped field: fix the rule or pipeline, not the gate.
  • Staging is noisy. Sample matches with an analyst before adding any filter.
  • Staging is silent. Replay a positive sample through the platform; a rule that cannot be shown to fire stays out of production.
Practice Trigger Owner Output
Requirement intake Intel, hunt, incident, audit finding Detection lead Request: threat, assets, data, priority
Search existing content Accepted request Engineer Community or vendor rule to adapt, run against your fixtures
Strategy before query Accepted request Engineer ADS-style strategy document
Test-first rule writing Strategy agreed Engineer Failing positive test, then rule, then negatives
Review and staged release Pull request, merge Second engineer, pipeline Approval; shadow-mode evidence
Disposition feedback Every closed alert SOC True, benign or false positive, or undetermined, per rule
Health checks Daily Pipeline Run status, data presence and canary result per rule
Emulation campaign Quarterly Purple team Detected or missed, per implementation
Retirement Obsolete, replaced, too costly Engineer deprecated status with reason and replacement

Search the community library first. For this page’s behaviour SigmaHQ already ships “Schedule Task Creation From Env Variable Or Potentially Suspicious Path Via Schtasks.EXE” and an event-4698 rule, “Suspicious Scheduled Task Creation”[26]. Run your fixtures against them, keep their filters where they fit and record how your version differs; the labs start from scratch only to teach each step.

Write the strategy before the query. Palantir’s ADS template is a compact checklist: Goal, Categorization, Strategy Abstract, Technical Context, Blind Spots and Assumptions, False Positives, Validation, Priority, Response[6]. Blind spots and validation keep coverage claims honest.

Write the failing test first. Panther recommends at least two tests per detection, “one false positive and one true positive”[27]. Add a known-gap test for malicious activity the rule deliberately misses, so any change to the gap forces the documentation to change (Lab 2).

Treat tuning as a change, not a click. A filter needs evidence, a scope (one rule, or a Sigma filter for many[28]), an owner and an expiry; CTID warns that filters “should be evaluated for the blind spots they may create”[21].

Source Type What it gives DetectionOps
Sigma Rules Specification v2.1.0 (August 2025)[24] Normative for Sigma files Rule structure, status and levels, conditions, correlation and filters
MITRE ATT&CK v20 (October 2026)[29] Knowledge base Techniques to map; Detection Strategies, Analytics, Data Components
Summiting the Pyramid (CTID)[20] Method Robustness levels, detection quality, implementation coverage, mapping rules
SANS detection engineering life cycle[23] Training reference Six stages; seven criteria for judging requests

Sigma in one paragraph. A rule needs a title, a logsource and a detection with a condition; the optional id should be a random version 4 UUID. “All values are treated as case-insensitive strings”, while regular expressions are case-sensitive by default; informational means “No case or alerting should be triggered”[24].

Every coverage report should carry CTID’s sentence “An ATT&CK tag alone is not evidence of detection coverage”[30]. Use three measures instead of one heatmap:

  1. Implementation coverage: how many of a technique’s implementations your rules detect, say “2/8 implementations”[22].
  2. Detection quality: how robust and precise is each covering rule[21]?
  3. Validation evidence: was the behaviour emulated recently, and did the rule fire?

Mappings decay. ATT&CK v18 (October 2025) replaced per-technique detection text with Detection Strategies and Analytics and deprecated Data Sources[31]; v19 (April 2026) split Defense Evasion into Stealth (TA0005) and Defense Impairment (TA0112)[32][33]. On 16 October 2026 the Sigma tags appendix still mapped defense-evasion to TA0005[34], while a SigmaHQ rule already carried attack.stealth[35]. Tactic counts can drop or double after a release with no change in what you detect; “maintaining those mappings is part of maintaining the detection”[30].

Named tools are examples verified on 2026-10-16, not recommendations.

Category Job Verified examples (October 2026) Selection notes
Native languages Full platform features Google SecOps YARA-L 2.0[36]; KQL, SPL, ES|QL Best fidelity; lock-in
Portable format and converter One rule, many platforms Sigma with sigma-cli 3.1.0 and pySigma backends[16][37] Conversion gaps; pin versions
Rule collections Starting content SigmaHQ (3,000+ rules)[10]; Elastic detection-rules[38] Needs local tests and tuning
Vendor DaC tooling Repository sync and tests Elastic DaC reference[39]; Sentinel repositories[40]; Panther tests[27]; Splunk contentctl[41] Product prerequisites
Threat-to-rule framework Link threats, objectives, rules OpenTide (EUPL 1.2)[1] Worth it when traceability is the pain
Tests and emulation Prove detections fire Atomic Red Team[42]; SigmaHQ regression samples[43] Authorised, isolated labs only

Facts that change decisions (October 2026; for Lab 3 and the judgement call). Sentinel repository connections need the Owner role on the workspace’s resource group; only Defender XDR custom detection rules (Preview) add a Microsoft 365 E5 or equivalent licence and Defender-portal onboarding. Sentinel in the Azure portal is unsupported after 31 March 2027[40], and Microsoft now recommends custom detections for new rules[44]. Splunk’s contentctl is “no longer accepting new pull requests or feature requests” as investment moves to Detection Studio[41].

Automate the deterministic steps (lint, tests, conversion, deployment, health checks, coverage reports); keep judgement (accepting a request, a filter or a gap) with people, on the record. SANS’s 2025 summary says 45% of organisations used AI for detection[3]; the sponsor’s 2026 summary says 83% of practitioners use AI tools but only 42% “trust them for core work like tuning detections”[7]. OpenTide argues that agents made writing a query cheap, but not knowing whether it “is valid, maps to a real threat and is deployed where it should be”[25]; generated rules pass the same tests, review and staging.

The reference implementation covers one behaviour: persistence through a Windows scheduled task whose action runs from a user-writable folder (ATT&CK T1053.005). It is safe to reproduce; ATT&CK lists a logon task running C:\Users\Public\test.exe[45], and DET0441 lists the telemetry: Security events 4698 (task created) and 4702 (task updated) and Sysmon process, file and registry events[18]. It also shows robustness in miniature: watching the schtasks.exe command line “might be easier for an adversary to evade than tracking task scheduler event IDs”[20]. So the repository holds a quick command-line rule (A) and a sturdier rule on task-creation and task-update events (B), each with tests and a strategy document.

Example status: structure and files executed in Labs 1–2 on 2026-10-17; deployment adapter not shown.

detections/
  rules/windows/
    proc_creation_win_schtasks_user_writable_action.yml   # rule A (command line)
    win_security_task_user_writable_action.yml            # rule B (events 4698, 4702)
  tests/
    d0fcbc79-de80-42d4-ab35-6f19f14047df.yml              # cases for rule A
    e71410f2-d333-4f3d-bb72-343d74241685.yml              # cases for rule B
  docs/strategies/                                         # one ADS-style file per rule
  tools/lint_rules.py  tools/run_tests.py  tools/deploy.py
  .github/workflows/detections.yml
  CODEOWNERS                                               # rules/ -> detection team

Rule A’s strategy document follows the ADS sections[6].

Example status: illustrative template, filled in for rule A.

Goal:          Detect persistence through scheduled tasks whose action runs
               from a user-writable folder.
Categorization: ATT&CK T1053.005 (Execution, Persistence, Privilege Escalation).
Strategy:      Sysmon Event ID 1 / process creation: schtasks.exe (by image or
               PE OriginalFileName) with /create or -create and an action
               under \Users\Public\, \AppData\, \ProgramData\, \Temp\ or
               %Public%-style environment variables (a teaching subset of
               the folders in the SigmaHQ rules).
Context:       Legitimate software rarely registers tasks that run from these
               folders; per-user updaters are the known exception.
Blind spots:   Tasks created through PowerShell, COM, the API or XML import,
               and existing tasks repointed with /change or the API (both
               covered by rule B, events 4698 and 4702); direct registry
               writes, which need SYSTEM and raise neither rule (see Lab 4);
               forward-slash paths such as C:/Users/Public/ (accepted; see
               Lab 2, step 4); malware written to the ChatApp updater path
               that registers its own task (accepted, see filter_chatapp).
False pos.:    Per-user updaters (benign positives); administrator testing.
Validation:    In an authorised lab host, create a run-once task whose action
               is a copy of a harmless built-in program in C:\Users\Public;
               confirm one alert; delete the task and the copy.
Priority:      Medium; High when the user is privileged or the host is a server.
Response:      Runbook SOC-RB-031: check the task, the binary's signer and
               the user's recent logons; escalate to ResponseOps if unknown.

Labs 1–2 need only Python 3.8+ and PyYAML; Lab 3 needs GitHub and a SIEM; Lab 4 is a planning exercise.

Lab 1 (beginner): lint rules against house rules

Link to section “Lab 1 (beginner): lint rules against house rules”

Objective. Catch malformed rules before review and enforce house rules Sigma leaves optional. Prerequisites. Python 3.8+, PyYAML (python3 -m pip install pyyaml). Cost and risk. None.

  1. Create rules/windows/, tests/ and tools/.
  2. Save the linter as tools/lint_rules.py:

Example status: executed on 2026-10-17 (Python 3.8.10, PyYAML 5.3.1).

"""House-rule linter for Sigma rules (teaching subset; also run sigma check)."""
import pathlib
import re
import sys
import uuid

import yaml

STATUS = {"stable", "test", "experimental", "deprecated", "unsupported"}
LEVEL = {"informational", "low", "medium", "high", "critical"}
TAG = re.compile(r"^[a-z0-9_-]+(\.[a-z0-9_.-]+)+$")
TECHNIQUE = re.compile(r"^attack\.t\d{4}(\.\d{3})?$")
DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def problems(rule, path, tests_dir):
    out = []
    if not rule.get("title") or len(rule["title"]) > 256:
        out.append("title missing or longer than 256 characters")
    try:
        if uuid.UUID(str(rule.get("id"))).version != 4:
            out.append("id is not a version 4 UUID")
    except ValueError:
        out.append("id missing or not a UUID")
    if rule.get("status") not in STATUS:
        out.append("status must be in %s" % sorted(STATUS))
    if rule.get("level") not in LEVEL:
        out.append("level must be in %s" % sorted(LEVEL))
    if not DATE.match(str(rule.get("date", ""))):
        out.append("date must be YYYY-MM-DD")
    src = rule.get("logsource") or {}
    if not any(k in src for k in ("category", "product", "service")):
        out.append("logsource needs category, product or service")
    if "condition" not in (rule.get("detection") or {}):
        out.append("detection.condition is missing")
    tags = rule.get("tags") or []
    bad = [t for t in tags if not TAG.match(t)]
    if bad:
        out.append("malformed tags: %s" % bad)
    if not any(TECHNIQUE.match(t) for t in tags):
        out.append("house rule: at least one attack.tNNNN technique tag")
    if not rule.get("falsepositives"):
        out.append("house rule: list known false positives (or 'Unknown')")
    if rule.get("status") == "deprecated" and not rule.get("related"):
        out.append("deprecated rule must name its replacement in related")
    test_file = tests_dir / ("%s.yml" % rule.get("id"))
    needs_tests = rule.get("status") in {"test", "stable", "experimental"}
    if needs_tests and not test_file.exists():
        out.append("house rule: no test file %s" % test_file.name)
    return out


def main(rules_dir="rules", tests_dir="tests"):
    failed = 0
    for path in sorted(pathlib.Path(rules_dir).rglob("*.yml")):
        rule = yaml.safe_load(path.read_text())
        found = problems(rule, path, pathlib.Path(tests_dir))
        print("%s %s" % ("FAIL" if found else "PASS", path))
        for item in found:
            print("     - " + item)
        failed += bool(found)
    print("%d rule(s) failed" % failed)
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))
  1. Save rule A, version 1, in rules/windows/ under the file name given in its first line:

Example status: executed on 2026-10-17 as input to Labs 1 and 2.

# file: proc_creation_win_schtasks_user_writable_action.yml
title: Scheduled Task Created With An Action In A User-Writable Folder
id: d0fcbc79-de80-42d4-ab35-6f19f14047df
status: experimental
description: >-
  Detects schtasks.exe creating a task whose action runs from a folder that
  any user can write to, a common persistence pattern.
references:
  - https://attack.mitre.org/techniques/T1053/005/
author: Detection team
date: 2026-10-16
tags:
  - attack.t1053.005
logsource:
  category: process_creation
  product: windows
detection:
  selection_img:
    Image|endswith: '\schtasks.exe'
  selection_create:
    CommandLine|contains: '/create'
  selection_path:
    CommandLine|contains:
      - '\Users\Public\'
      - '\Temp\'
  condition: all of selection_*
falsepositives:
  - Per-user software updaters that install under the user profile
level: medium
  1. Save a deliberately broken rule as rules/windows/broken_example.yml (id: 1234, status: production, date: 16/10/2026, level: severe, the tag ATT&CK T1053, no falsepositives) and run python3 tools/lint_rules.py.

Expected output (executed):

FAIL rules/windows/broken_example.yml
     - id missing or not a UUID
     - status must be in ['deprecated', 'experimental', 'stable', 'test', 'unsupported']
     - level must be in ['critical', 'high', 'informational', 'low', 'medium']
     - date must be YYYY-MM-DD
     - malformed tags: ['ATT&CK T1053']
     - house rule: at least one attack.tNNNN technique tag
     - house rule: list known false positives (or 'Unknown')
FAIL rules/windows/proc_creation_win_schtasks_user_writable_action.yml
     - house rule: no test file d0fcbc79-de80-42d4-ab35-6f19f14047df.yml
2 rule(s) failed
  1. Delete the broken rule. Rule A still fails until it has tests.

Validation. Exit code 1 while any rule fails, 0 once all pass (after Lab 2 step 1). Troubleshooting. ScannerError: use single quotes for Windows paths. Teardown. Keep the folder for Lab 2.

Lab 2 (intermediate): test-first tuning and the filter trade-off

Link to section “Lab 2 (intermediate): test-first tuning and the filter trade-off”

Objective. Write positive, negative, out-of-scope and known-gap tests, watch version 1 fail, improve it, and measure a filter’s precision–recall trade-off. Prerequisites. Lab 1. Cost and risk. None; synthetic events.

  1. Save the cases as tests/d0fcbc79-de80-42d4-ab35-6f19f14047df.yml (the first follows an ATT&CK procedure example[45]):

Example status: executed on 2026-10-17.

rule_id: d0fcbc79-de80-42d4-ab35-6f19f14047df
cases:
  - name: logon task runs from Users\Public
    expect: match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /create /tn "mysc" /tr C:\Users\Public\test.exe
        /sc ONLOGON /ru "System"
  - name: action in a roaming profile
    expect: match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /CREATE /tn Sync /sc daily
        /tr C:\Users\alice\AppData\Roaming\sync\s.exe
  - name: renamed copy of schtasks
    expect: match
    event:
      Image: 'C:\Users\Public\tsk.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'tsk.exe /create /tn upd /tr C:\Users\Public\u.exe /sc onlogon'
  - name: dash switches
    expect: match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'schtasks -create -tn upd -tr C:\Users\Public\u.exe -sc onlogon'
  - name: unexpanded environment variable
    expect: match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'schtasks /create /tn upd /tr %PUBLIC%\u.exe /sc onlogon'
  - name: action under ProgramData
    expect: match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'schtasks /create /tn upd /tr C:\ProgramData\u.exe /sc onlogon'
  - name: hides behind the updater path
    expect: match
    event:
      ParentImage: 'C:\Windows\System32\cmd.exe'
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /create /sc onlogon /tr C:\Users\Public\u.exe
        /tn C:\Users\eve\AppData\Local\ChatApp\Update.exe
  - name: vendor updater in Program Files
    expect: no_match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /create /tn "Vendor\Update" /sc daily
        /tr "C:\Program Files\Vendor\update.exe"
  - name: administrator lists tasks
    expect: no_match
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'schtasks /query /fo LIST /v'
  - name: chat app per-user updater
    expect: no_match
    event:
      ParentImage: 'C:\Users\bob\AppData\Local\ChatApp\Update.exe'
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /create /tn "ChatApp Update" /sc daily
        /tr "C:\Users\bob\AppData\Local\ChatApp\Update.exe --check" /f
  - name: malware at the updater path
    expect: known_gap
    owner: filter_chatapp
    event:
      ParentImage: 'C:\Users\eve\AppData\Local\ChatApp\Update.exe'
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: >-
        schtasks /create /tn "ChatApp Update" /sc onlogon
        /tr C:\Users\eve\AppData\Local\ChatApp\Update.exe
  - name: task registered from PowerShell
    expect: out_of_scope
    owner: rule e71410f2
    event:
      Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
      OriginalFileName: PowerShell.EXE
      CommandLine: 'powershell.exe -File C:\Users\Public\register.ps1'
  - name: task repointed with /change
    expect: out_of_scope
    owner: rule e71410f2
    event:
      Image: 'C:\Windows\System32\schtasks.exe'
      OriginalFileName: schtasks.exe
      CommandLine: 'schtasks /change /tn Existing /tr C:\Users\Public\u.exe'
  1. Save the runner as tools/run_tests.py: a teaching subset of Sigma with case-insensitive matching as the specification requires[24]. It ignores logsource, which real backends map through pipelines.

Example status: executed on 2026-10-17 (Python 3.8.10, PyYAML 5.3.1). Teaching code, not a replacement for pySigma.

"""Run labelled events against Sigma rules (teaching subset of Sigma).

Supports: maps and lists of maps; modifiers contains, startswith, endswith, all,
windash; * and ? wildcards; conditions with and/or/not, brackets, '1 of x*',
'all of x*'. Matching is case-insensitive, as the Sigma specification requires.
"""
import fnmatch
import itertools
import pathlib
import re
import sys

import yaml

DASHES = ("-", "/", "\u2013", "\u2014", "\u2015")  # windash characters


def windash(value):
    """Every variant of value with each flag's leading - or / swapped."""
    parts = re.split(r"\B[-/]\b", value)
    return ["".join(p + d for p, d in zip(parts, combo + ("",)))
            for combo in itertools.product(DASHES, repeat=len(parts) - 1)]


def value_matches(actual, expected, mods):
    if "windash" in mods:
        rest = [m for m in mods if m != "windash"]
        return any(value_matches(actual, v, rest) for v in windash(str(expected)))
    a, e = str(actual).lower(), str(expected).lower()
    if "contains" in mods:
        return e in a
    if "startswith" in mods:
        return a.startswith(e)
    if "endswith" in mods:
        return a.endswith(e)
    return fnmatch.fnmatchcase(a, e)


def map_matches(event, mapping):
    for key, expected in mapping.items():
        field, *mods = key.split("|")
        if field not in event:
            return False
        values = expected if isinstance(expected, list) else [expected]
        hits = [value_matches(event[field], v, mods) for v in values]
        if not (all(hits) if "all" in mods else any(hits)):
            return False
    return True


def search_matches(event, search):
    if isinstance(search, list):  # list of maps = OR
        return any(map_matches(event, item) for item in search)
    return map_matches(event, search)


TOKEN = re.compile(r"\(|\)|1 of [\w*]+|all of [\w*]+|\band\b|\bor\b|\bnot\b|[\w*]+")


def evaluate(condition, results):
    tokens = TOKEN.findall(condition)
    pos = [0]

    def peek():
        return tokens[pos[0]] if pos[0] < len(tokens) else None

    def take():
        pos[0] += 1
        return tokens[pos[0] - 1]

    def group(tok):
        quant, pattern = tok.split(" of ")
        names = [n for n in results if fnmatch.fnmatchcase(n, pattern)]
        hits = [results[n] for n in names]
        return all(hits) if quant == "all" else any(hits)

    def atom():
        tok = take()
        if tok == "(":
            val = expr()
            take()  # ")"
            return val
        if tok == "not":
            return not atom()
        if " of " in tok:
            return group(tok)
        return results[tok]

    def term():
        val = atom()
        while peek() == "and":
            take()
            rhs = atom()
            val = val and rhs
        return val

    def expr():
        val = term()
        while peek() == "or":
            take()
            rhs = term()
            val = val or rhs
        return val

    return expr()


def rule_matches(rule, event):
    det = rule["detection"]
    results = {k: search_matches(event, v)
               for k, v in det.items() if k != "condition"}
    return evaluate(det["condition"], results)


def main(rules_dir="rules", tests_dir="tests"):
    rules = {}
    for path in pathlib.Path(rules_dir).rglob("*.yml"):
        rule = yaml.safe_load(path.read_text())
        rules[rule["id"]] = rule
    tp = fp = fn = tn = failures = 0
    for test_path in sorted(pathlib.Path(tests_dir).glob("*.yml")):
        suite = yaml.safe_load(test_path.read_text())
        rule = rules[suite["rule_id"]]
        print("%s (%s)" % (rule["title"], rule["status"]))
        for case in suite["cases"]:
            fired = rule_matches(rule, case["event"])
            want = case["expect"]
            if want == "out_of_scope":
                print("  SKIP %-34s fired=%s owner=%s"
                      % (case["name"], fired, case["owner"]))
                continue
            if want == "known_gap":  # malicious, accepted blind spot
                fn += not fired
                failures += fired  # gap closed: update the test
                print("  GAP  %-34s fired=%s accepted=%s"
                      % (case["name"], fired, case["owner"]))
                continue
            ok = fired == (want == "match")
            failures += not ok
            tp += fired and want == "match"
            fp += fired and want == "no_match"
            fn += (not fired) and want == "match"
            tn += (not fired) and want == "no_match"
            print("  %s %-34s expect=%-8s fired=%s"
                  % ("PASS" if ok else "FAIL", case["name"], want, fired))
    precision = tp / (tp + fp) if tp + fp else float("nan")
    recall = tp / (tp + fn) if tp + fn else float("nan")
    print("TP=%d FP=%d FN=%d TN=%d precision=%.2f recall=%.2f failures=%d"
          % (tp, fp, fn, tn, precision, recall, failures))
    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))
  1. Run python3 tools/run_tests.py against version 1. Expected output (executed; exit code 1):
Scheduled Task Created With An Action In A User-Writable Folder (experimental)
  PASS logon task runs from Users\Public  expect=match    fired=True
  FAIL action in a roaming profile        expect=match    fired=False
  FAIL renamed copy of schtasks           expect=match    fired=False
  FAIL dash switches                      expect=match    fired=False
  FAIL unexpanded environment variable    expect=match    fired=False
  FAIL action under ProgramData           expect=match    fired=False
  PASS hides behind the updater path      expect=match    fired=True
  PASS vendor updater in Program Files    expect=no_match fired=False
  PASS administrator lists tasks          expect=no_match fired=False
  PASS chat app per-user updater          expect=no_match fired=False
  GAP  malware at the updater path        fired=False accepted=filter_chatapp
  SKIP task registered from PowerShell    fired=False owner=rule e71410f2
  SKIP task repointed with /change        fired=False owner=rule e71410f2
TP=2 FP=0 FN=6 TN=3 precision=1.00 recall=0.25 failures=5
  1. Replace the detection block with version 2. It accepts the name compiled into the executable’s header (OriginalFileName), adds \AppData\, \ProgramData\ and environment variables, and uses windash because “Windows command line flags can often be indicated by both characters”[46]. Its filter for the updater the SOC reported checks two fields, parent process and action path; an endswith on the whole command line would hide the “hides behind the updater path” case and miss this updater’s quoted /tr. The path lists are a teaching subset: production rules add folders such as \Perflogs\, \Downloads\ and \Desktop\ (compare the SigmaHQ rules[26]); forward-slash spellings (C:/Users/Public/) match neither rule and are an accepted gap in the strategy.

Example status: executed on 2026-10-17.

detection:
  selection_img:
    - Image|endswith: '\schtasks.exe'
    - OriginalFileName: 'schtasks.exe'
  selection_create:
    CommandLine|contains|windash: '/create'
  selection_path:
    CommandLine|contains:
      - '\Users\Public\'
      - '\AppData\'
      - '\ProgramData\'
      - '\Temp\'
      - '%Public%'
      - '%AppData%'
      - '%LocalAppData%'
      - '%Temp%'
      - '%TMP%'
  filter_chatapp:
    ParentImage|endswith: '\AppData\Local\ChatApp\Update.exe'
    CommandLine|contains: '\AppData\Local\ChatApp\Update.exe'
  condition: all of selection_* and not filter_chatapp
  1. Run the tests again. Expected output (executed; exit code 0):
Scheduled Task Created With An Action In A User-Writable Folder (experimental)
  PASS logon task runs from Users\Public  expect=match    fired=True
  PASS action in a roaming profile        expect=match    fired=True
  PASS renamed copy of schtasks           expect=match    fired=True
  PASS dash switches                      expect=match    fired=True
  PASS unexpanded environment variable    expect=match    fired=True
  PASS action under ProgramData           expect=match    fired=True
  PASS hides behind the updater path      expect=match    fired=True
  PASS vendor updater in Program Files    expect=no_match fired=False
  PASS administrator lists tasks          expect=no_match fired=False
  PASS chat app per-user updater          expect=no_match fired=False
  GAP  malware at the updater path        fired=False accepted=filter_chatapp
  SKIP task registered from PowerShell    fired=False owner=rule e71410f2
  SKIP task repointed with /change        fired=False owner=rule e71410f2
TP=7 FP=0 FN=1 TN=3 precision=1.00 recall=0.88 failures=0
  1. See the trade-off. Remove and not filter_chatapp and rerun: on 2026-10-17 the result was TP=7 FP=1 FN=0 TN=2 precision=0.88 recall=1.00 failures=2. Without the filter the updater alerts (in production, a benign positive); with it, malware written to that path is invisible, and the known-gap test records the decision. Restore the filter.

  2. Cover the out-of-scope cases with rule B. Save the rule on events 4698 and 4702 and its tests, then run a third time.

Example status: executed on 2026-10-17.

title: Scheduled Task Registered Or Updated To Run From A User-Writable Folder
id: e71410f2-d333-4f3d-bb72-343d74241685
status: experimental
description: >-
  Detects any scheduled task registration or update (schtasks, PowerShell,
  COM or API) whose action runs from a user-writable folder, using Security
  events 4698 (task created) and 4702 (task updated).
references:
  - https://attack.mitre.org/detectionstrategies/DET0441/
author: Detection team
date: 2026-10-16
tags:
  - attack.t1053.005
logsource:
  product: windows
  service: security
  definition: Requires the audit subcategory "Audit Other Object Access Events"
detection:
  selection_created:
    EventID: 4698
    TaskContent|contains:
      - '\Users\Public\'
      - '\AppData\'
      - '\ProgramData\'
      - '\Temp\'
      - '%Public%'
      - '%AppData%'
      - '%LocalAppData%'
      - '%Temp%'
      - '%TMP%'
  selection_updated:
    EventID: 4702
    TaskContentNew|contains:
      - '\Users\Public\'
      - '\AppData\'
      - '\ProgramData\'
      - '\Temp\'
      - '%Public%'
      - '%AppData%'
      - '%LocalAppData%'
      - '%Temp%'
      - '%TMP%'
  condition: 1 of selection_*
falsepositives:
  - Per-user software updaters that install under the user profile
level: medium
rule_id: e71410f2-d333-4f3d-bb72-343d74241685
cases:
  - name: task registered from PowerShell
    expect: match
    event:
      EventID: 4698
      TaskName: '\Updater'
      TaskContent: '<Exec><Command>C:\Users\Public\u.exe</Command></Exec>'
  - name: action as an environment variable
    expect: match
    event:
      EventID: 4698
      TaskName: '\Upd'
      TaskContent: '<Exec><Command>%PUBLIC%\u.exe</Command></Exec>'
  - name: existing task repointed to TMP
    expect: match
    event:
      EventID: 4702
      TaskName: '\Microsoft\StartListener'
      TaskContentNew: '<Exec><Command>%TMP%\listener.exe</Command></Exec>'
  - name: vendor task in Program Files
    expect: no_match
    event:
      EventID: 4698
      TaskName: '\Vendor\Update'
      TaskContent: '<Exec><Command>C:\Program Files\Vendor\update.exe</Command></Exec>'
  - name: vendor update in Program Files
    expect: no_match
    event:
      EventID: 4702
      TaskName: '\Vendor\Update'
      TaskContentNew: '<Exec><Command>C:\Program Files\Vendor\update.exe</Command></Exec>'
  - name: inline encoded PowerShell action
    expect: known_gap
    owner: backlog DR-0142
    event:
      EventID: 4698
      TaskName: '\Sync'
      TaskContent: >-
        <Exec><Command>powershell.exe</Command>
        <Arguments>-enc BASE64_PAYLOAD</Arguments></Exec>

Expected output of the third run (executed; last lines, exit code 0):

Scheduled Task Registered Or Updated To Run From A User-Writable Folder (experimental)
  PASS task registered from PowerShell    expect=match    fired=True
  PASS action as an environment variable  expect=match    fired=True
  PASS existing task repointed to TMP     expect=match    fired=True
  PASS vendor task in Program Files       expect=no_match fired=False
  PASS vendor update in Program Files     expect=no_match fired=False
  GAP  inline encoded PowerShell action   fired=False accepted=backlog DR-0142
TP=10 FP=0 FN=2 TN=5 precision=1.00 recall=0.83 failures=0

What to notice. Event 4698 exists only if “Audit Other Object Access Events” is enabled[47], and Sigma’s definition is documentation, not configuration[24]: put the dependency in the request and a health check. Rule B ignores which tool registered the task, provided Task Scheduler logs it, and sees a task repointed with schtasks /change or the API through event 4702, whose new definition is in TaskContentNew, not TaskContent[48]. A task written straight into the registry (needs SYSTEM and a service restart) “can bypass the generation of scheduled task creation event logs, such as 4698 and 106”[49]; look for it in DET0441’s Sysmon registry events[18]. Validation. Break one positive fixture and confirm the run fails. Troubleshooting. KeyError: a test’s rule_id matches no rule id. Teardown. Keep the folder for Lab 3.

Lab 3 (advanced): a CI pipeline with staged deployment

Link to section “Lab 3 (advanced): a CI pipeline with staged deployment”

Objective. Validate every pull request, deploy merges in shadow mode, and promote only from a reviewed release tag on main. Prerequisites. A private GitHub repository with the Lab 2 folder; a SIEM API with a least-privileged token per environment; your own tools/deploy.py adapter (for Splunk, around the saved/searches REST endpoint; not shown). Cost and risk. CI minutes; a bad production deploy can flood the SOC.

Example status: syntax-checked, not executed (needs GitHub, a SIEM and Python 3.10 or later for sigma-cli 3.1.0). Action majors are current as of October 2026[50]; pin each action to a full commit SHA in production.

name: detections
on:
  pull_request:
    paths: ["rules/**", "tests/**", "tools/**", ".github/workflows/**"]
  push:
    branches: [main]
    tags: ["release-*"]
permissions:
  contents: read
jobs:
  validate:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7      # pin to a full commit SHA in production
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - name: Install the pinned toolchain
        run: >-
          python -m pip install "sigma-cli==3.1.0" "pysigma==1.5.1"
          "pysigma-backend-splunk==2.1.0" "pysigma-pipeline-sysmon==2.0.0"
          "pysigma-pipeline-windows==2.0.1" "pyyaml==6.0.3"
      - name: House rules and unit tests
        run: |
          python tools/lint_rules.py
          python tools/run_tests.py
      - name: Sigma validators (fail on issues, not only on errors)
        run: |
          mkdir -p build
          sigma check --fail-on-issues --junitxml build/sigma-check.xml rules/
      - name: Convert for Splunk
        run: >-
          sigma convert -t splunk -p sysmon -p windows-logsources
          -f savedsearches -o build/savedsearches.conf rules/
      - uses: actions/upload-artifact@v7
        with:
          name: converted
          path: build/
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    needs: validate
    runs-on: ubuntu-24.04
    environment: detections-staging      # deployment branch rule: main only
    steps:
      - uses: actions/checkout@v7
      - uses: actions/download-artifact@v8
        with:
          name: converted
          path: build/
      - name: Deploy in shadow mode (no notable events, no paging)
        env:
          SIEM_TOKEN: ${{ secrets.SIEM_TOKEN }}
        run: python tools/deploy.py --mode shadow build/savedsearches.conf
  deploy-production:
    if: startsWith(github.ref, 'refs/tags/release-')
    needs: validate
    runs-on: ubuntu-24.04
    environment: detections-production   # tag rule release-*, required reviewer
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0                 # all branches, for the next step
      - name: Refuse a tag whose commit is not on main
        run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main
      - uses: actions/download-artifact@v8
        with:
          name: converted
          path: build/
      - name: Promote to alerting
        env:
          SIEM_TOKEN: ${{ secrets.SIEM_TOKEN }}
        run: python tools/deploy.py --mode alert build/savedsearches.conf

Why each line is there.

  • sigma check passes on validation issues unless you add --fail-on-issues[51].
  • paths includes the workflow folder, so a pin bump runs validate instead of shipping untested or, if validate is a required check, never merging.
  • Conversion follows the sigma-cli README[16]. The Sysmon pipeline rewrites only generic categories such as process_creation; windows-logsources adds rule B’s Windows Channel condition[52]. Check the converted rule B for it.
  • Who can reach each SIEM token. The validate job runs unreviewed pull-request code: contents: read, no environment. Keep each token only as an environment secret, never a repository or organisation secret: such secrets “are only available to workflow jobs that reference the environment”[53], on any branch unless restricted. Limit detections-staging to main with a deployment branch rule, and protect main with required review and status checks; the if: lines are convenience, not control.
  • What makes a release reviewed. A deployment tag rule matches only the name in GITHUB_REF[53]; any writer can push release-x on any commit unless a tag ruleset restricts creating and updating release-* to maintainers[54]. The ancestry step refuses tags outside main. On detections-production, require reviewers, prevent self-review and disallow the default administrator bypass; a job awaiting approval cannot read the environment’s secrets.
  • Plan limits (October 2026). In private repositories, environments, their secrets and branch and tag rules need GitHub Pro, Team or Enterprise[55]; rulesets need Pro, Team or Enterprise Cloud[54]; required reviewers need Enterprise[53][55], and self-hosted runners are not isolated[53]. Each token may only create and update detections in one app or workspace.

Expected output. A pull request runs only validate; a merge to main runs deploy-staging; a release-* tag on main waits for a reviewer, then deploys; a tag elsewhere fails the ancestry step. Validation. A broken fixture fails the pull request and deploys nothing; a branch workflow naming detections-staging is refused. Troubleshooting. ResolutionImpossible from pip: one package moved to pySigma 2 early; keep pins together. Teardown. Delete the test tag and staging searches; revoke lab tokens.

Lab 4 (advanced, planning exercise): design one validation run

Link to section “Lab 4 (advanced, planning exercise): design one validation run”

Objective. Turn “we detect scheduled-task persistence” into evidence. Prerequisites. An authorised, isolated lab host sending telemetry to staging; the SOC informed. Status. A worksheet, nothing executed. Its Expected output, Validation and Teardown lines are the acceptance criteria:

Behaviour:        T1053.005 Scheduled Task (persistence)
Implementations:  1 schtasks.exe   2 PowerShell cmdlet   3 COM/API   4 XML import
                  5 direct registry write (needs SYSTEM)
                  6 existing task repointed (schtasks /change or the API)
Expected signal:  impl 1 -> rules A and B;  impl 2-4 -> rule B only (4698);
                  impl 5 -> neither: known gap (look at Sysmon 13/14, DET0441);
                  impl 6 -> rule B only (4702)
Test source:      Atomic Red Team test chosen by GUID (GUIDs do not change)
Harmless payload: copy of a built-in program; no network, no credentials
Expected output:  each implementation raises its expected alert within 15 min
Validation:       alert and raw event fall inside the run's window, per GUID
Evidence kept:    alert IDs, raw events, test GUID, time window, engineer
Teardown:         delete the task and the copied file; confirm with a query
Stop condition:   any unexpected impact on the host or the SOC queue

Atomic Red Team tests are designed to run “in five minutes or less”, many with cleanup commands[56]; select them by GUID because GUIDs “are guaranteed to not change”[57]; the detected-or-missed record per implementation feeds validated coverage.

1. Sourced: the European Commission’s security operations centre. Before OpenTide, threat information went into spreadsheets whose “rows could not be linked to the rules they deployed”, analyst Claus Houmann recalls. Since September 2022, Remi Seguy’s team has used it to “manage hundreds of threat vectors, each linked to detection objectives and deployed detection rules”; Seguy says “Every rule is validated, greatly reducing the risk of errors”[12]. Lesson: traceability was the problem; tooling followed. Caveat: the Commission’s own account; no independent evaluation found.

Measure the library, not people; the formulas are the Atlas’s proposals. Precision uses SOC Ops dispositions: benign positives count against it like false positives, since both cost analyst time, but need a different fix (context or a scoped filter); undetermined alerts are reported separately. Teams judging logic, not analyst load, may count benign positives as correct: Magonia classes expected activity as a true positive with “No Business Impact”, defines precision as “how often the rule fires correctly” and adds a separate impact rate[58]. Label each report’s convention.

KPI Formula Data source Pitfall
Rule precision TP ÷ (TP + benign positives + FP), per rule, 30 days SOC dispositions Undetermined or unlabelled alerts
Validated coverage prioritised implementations detected in emulation within 90 days ÷ prioritised implementations Emulation records Counting tags, not runs
Test completeness rules with ≥ 1 positive and ≥ 1 negative test ÷ production rules Repository Tests that match by construction
Rule health rules whose runs, data and canary checks passed in 7 days ÷ production rules Health jobs, rule-run logs Checking that a query runs, not that it can match
Detection latency median and p95 of (alert time − event time) per rule Emulation runs, true positives Ingestion delay hidden by event-time filters
Detection lead time median days from accepted request to production Backlog, Git Splitting work to look fast
Change failure rate deployments rolled back or hot-fixed within 7 days ÷ deployments Git, change log Silent fix-forwards
Filter debt filters past expiry or without owner ÷ filters Repository Filters hidden in consoles

Test completeness, rule health and filter debt lead; validated coverage lags but is closest to the truth; lead time and change failure rate come from DevOps. The sponsor’s 2026 summary reports 59% of teams track false-positive rates but only 14% prioritise reducing them[7].

Worked calculation (illustrative numbers). In September a team has 120 production rules.

  • Rule A: 40 alerts, 6 true, 30 benign and 4 false positives. Precision = 6 ÷ (6 + 30 + 4) = 15%: noisy-rule review.
  • 84 rules have a positive and a negative test. Test completeness = 84 ÷ 120 = 70%.
  • Emulation in 90 days detected 18 of 30 prioritised implementations. Validated coverage = 18 ÷ 30 = 60%; a tag heatmap shows 100%.
  • 114 rules passed health checks. Rule health = 114 ÷ 120 = 95%.
  • 3 of 50 deployments were rolled back within a week. Change failure rate = 3 ÷ 50 = 6%.

Set targets per rule class: a rule that pages on-call may need precision above 50%; a low-severity rule feeding a correlation can tolerate less.

  • The repository is sensitive. It lists what you watch and, through filters and known gaps, what you do not. Palantir notes the “operational security considerations around publicly acknowledging and documenting internal alerts”[6]; the Atlas adds: keep it private, use code owners, and require review for filter changes.
  • Pipeline credentials are scoped per environment (Lab 3). ATT&CK’s Defense Impairment tactic covers attempts to “break security mechanisms, pipelines, and tooling”[33], yours included.
  • Test data. Prefer synthetic or sanitised samples; real events carry user and host names (PrivacyOps).
  • Auditability. Git history shows who changed which detection and why. Report against NIST CSF 2.0 outcomes DE.CM and DE.AE[59]; CSF is voluntary unless mandated; sector monitoring duties belong to ComplianceOps.

Telemetry: Microsoft warns that Sysmon ProcessAccess events “can generate significant amounts of logging”[60]; requests state their data needs, and SecDataOps decides ingest. Compute: long look-backs, correlations and real-time rules consume SIEM capacity. People: SANS’s 2025 summary lists high false-positive rates in vendor detection tools (64%) among the most-cited technology challenges, and 71% cite resource and time constraints as their primary obstacle[3]. A useful unit is analyst minutes per true positive: (alerts × average triage minutes) ÷ true positives. Rule A’s 40 alerts at 6 minutes for 6 true positives cost 40 minutes each. Retirement is the cheapest capacity.

  • Sampled review: each month, check five random production rules for requirement, strategy, tests, owner and review date.
  • Deployed equals repository: a nightly job reports drift between platforms and Git (console edits, disabled rules).
  • Gap register: known-gap tests and expired filters appear in the quarterly coverage report.
  • Independent validation: an annual exercise by a red team tests implementations the detection team did not choose.
Symptom Likely cause Diagnosis Remediation and prevention
Rule silent, data present Rule disabled, runs failing or skipped, deployed query drifted from Git Rule-health log[61]; diff with Git Redeploy from Git; alert on failed runs and drift
Rule stopped firing, no errors Field renamed, parser or audit policy changed Positive fixture against live raw events Fix mapping; hunt the gap window
Late events never alert Look-back equals the run interval; late-ingested events fall between runs Event versus ingestion time per source Widen the look-back by the delay; deduplicate the overlap[44]
Alert flood after a deploy Logic error, missing filter, wrong index Diff with the previous version Roll back via the pipeline; enforce staging
Weeks of noise, analysts ignore it No tuning loop or owner Precision per rule from dispositions Tune, downgrade to informational, or retire
CI breaks with no rule change Unpinned toolchain Package versions versus the last green run Pin together; planned upgrades
Passes tests, misses in emulation Fixtures unlike real telemetry, or an uncovered implementation Capture the run’s raw events Add real regression samples[43]

Situation (illustrative). Rule B (events 4698 and 4702) usually raises 2–5 alerts a week; the weekly review notices eleven silent days.

  1. Did the deployed rule run? Check the rule-run log and diff the deployed query with Git. In Microsoft Sentinel, with health monitoring on, the SentinelHealth table records for each run “if they succeeded or failed, and if they failed, why”, and shows auto-disabled rules and skipped windows[61]. Result: all runs succeeded; the query matches Git.
  2. Is the data arriving? Count events 4698 and 4702 per day and host for 14 days. Result: it stopped on 80% of hosts on one day.
  3. What changed? Check the change calendar (SecOps). Result: a Group Policy change rebuilt the workstation audit policy without “Audit Other Object Access Events”, the subcategory that produces both events[47][48].
  4. Contain the blind spot. Open a data-gap incident with SecDataOps and the policy owner; meanwhile raise rule A’s priority, knowing it sees only the schtasks.exe path.
  5. Hunt the gap window. Ask ThreatOps to hunt the eleven days with DET0441’s other telemetry (Sysmon process and registry events)[18].
  6. Restore and verify through the deployed rule. After the fix, repeat the Lab 4 validation on one lab host; the alert must come from the production rule, not a fixture.
  7. Prevent recurrence. Alert when a host sends Security events but no 4698 for 72 hours, and on failed or skipped runs; add the audit subcategory to the rule’s dependencies and to the audit-policy change checklist.

Exit criteria. Runs healthy; 4698 on all monitored hosts; validation alert from the production rule; gap hunt closed.

Figure 5. Runbook flow when a detection goes silent
yesyesyesyesnonononoSilent rule: ran, and matchesGit?Raw events arriving on time?Fixture matches live fields?Canary fires through thedeployed rule?Quiet is real: record itFix the run or redeployData gap or delay:SecDataOpsFix mapping or rule
Diagram source (Mermaid)
flowchart TB
  R["Silent rule: ran, and matches Git?"]
  B["Raw events arriving on time?"]
  D["Fixture matches live fields?"]
  F["Canary fires through the deployed rule?"]
  Q["Quiet is real: record it"]
  RX["Fix the run or redeploy"]
  C["Data gap or delay: SecDataOps"]
  E["Fix mapping or rule"]
  R -->|"yes"| B
  B -->|"yes"| D
  D -->|"yes"| F
  F -->|"yes"| Q
  R -->|"no"| RX
  B -->|"no"| C
  D -->|"no"| E
  F -->|"no"| E
  RX ~~~ C
  C ~~~ E
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  class R,B,D,F decision
  class Q core
  class RX,C,E risk

Text equivalent: check in order that the deployed rule ran and matches Git (else fix or redeploy); that raw events arrive in time (else a data gap or delay for SecDataOps); that the fixture matches live fields (else fix mapping or rule); and that a canary fires through the deployed rule (else fix the rule). Only then is the quiet real.

  • “We cover 80% of ATT&CK.” A tag count, not coverage; report implementations and validation evidence[30].
  • Console hot-fixes. A night-time exclusion in the SIEM UI never reaches Git; the next deploy erases it.
  • Tests written from the rule. Fixtures derived from the query pass by construction; derive them from the threat and real benign activity.
  • Importing a pack wholesale. SigmaHQ’s 3,000+ rules[10] and vendor content are starting points to adapt and test one by one; enabling everything buys noise and false confidence.
  • Indicator rules as detections. Hash and IP matches sit on the two lowest levels of the Pyramid of Pain[8]; give them expiry dates and behavioural replacements.
  • Myth: detection-as-code needs a big platform. Labs 1–3 need Git, two scripts, a CI runner and one API.

Adopt the full practice when you write or change custom detections, more than one person edits them, the SOC reports noise or misses, or nobody can answer “what do we detect?” with evidence; multi-platform and multi-tenant teams gain most. Use a light version with mainly vendor or MDR content: custom rules and exclusions in Git, filter reviews with the provider, periodic emulation.

Do not start here when:

  • The data is missing. Start with SecDataOps; SANS’s 2025 summary says 47% of respondents report inadequate access to the data feeds needed[3].
  • Nobody works the alerts. Better detections become better-ignored alerts; start with SOC Ops.
  • Prevention basics are missing. The same effort may reduce more risk in VulnerabilityOps or IAMOps.
  • The platform has no content API. Keep the repository as documentation and reconcile by export.

Hidden responsibilities: someone must own the toolchain (pins, upgrades, remapping), health checks and the emulation lab.

Figure 6. Should you adopt detection-as-code, and how much of it?
noyesnoyesnoyesnoyesCustom detections?Telemetry parsed?Alerts triaged?Several platforms?Light versionFix data firstFix triage firstNative queries as codeSigma or threat model
Diagram source (Mermaid)
flowchart TB
  Q1["Custom detections?"]
  Q2["Telemetry parsed?"]
  Q3["Alerts triaged?"]
  Q4["Several platforms?"]
  L1["Light version"]
  L2["Fix data first"]
  L3["Fix triage first"]
  L4["Native queries as code"]
  L5["Sigma or threat model"]
  Q1 -->|"no"| L1
  Q1 -->|"yes"| Q2
  Q2 -->|"no"| L2
  Q2 -->|"yes"| Q3
  Q3 -->|"no"| L3
  Q3 -->|"yes"| Q4
  Q4 -->|"no"| L4
  Q4 -->|"yes"| L5
  L1 ~~~ L2
  L2 ~~~ L3
  L3 ~~~ L4
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  class Q1,Q2,Q3,Q4 decision
  class L1,L4,L5 core
  class L2,L3 risk

Text equivalent: four questions in order. Do you write or change custom detections? If not, use the light version. Is telemetry collected and parsed? If not, fix data first with SecDataOps. Is anyone triaging the alerts? If not, fix triage first with SOC Ops. Several platforms or tenants? If not, keep native queries as code; if so, use Sigma or a threat-to-rule model.

For tool choices, score candidates 1–5 against weights you can defend; an example for a single-SIEM team:

Criterion Weight Why
Content API or repository connector 25% Otherwise deployment stays manual
Fidelity to the platform features you use 20% Lost features become blind spots
Testability: fixtures, replay, regression samples 20% Tests are the core of the practice
Toolchain stability and licence 15% See the pySigma and contentctl notes
Team skills 10% SANS 2026 (sponsor summary): 13% of detection engineers report high software-engineering proficiency[7]
Portability 10% A SIEM migration means rewriting every native rule

This is the Atlas’s model, not a standard. It parallels Elastic’s five DEBMM tiers[4] and the Detection Engineering Maturity Matrix[11], with exit criteria you can check in a repository.

Level Observable behaviour Exit criteria (evidence)
0 Ad hoc Rules created and edited in consoles; no owner —
1 Versioned Custom rules and filters in Git with owner, status and technique tags Nightly drift report shows zero drift
2 Tested Positive and negative tests; CI lints, tests and converts; reviews required Test completeness ≥ 90%; no merge without green CI
3 Validated Staging for every change; precision per rule; quarterly emulation Validated coverage reported per implementation
4 Managed portfolio Threat-ranked backlog; health checks; expiring filters; routine retirement Rule health ≥ 95%; filter debt near zero; coverage used in risk decisions

Elastic warns that rushing tiers “without proper budget and staff can lead to burnout”[4]. Reach level 3 for your top threats before level 2 everywhere.

Window Actions Milestone and metric
Days 1–30 Export custom rules and exclusions to a private repository; add owner, status, technique tags; Lab 1 linting in CI Inventory complete; drift report running
Days 31–60 Tests for the 20 noisiest or most severe rules; conversion and sigma check --fail-on-issues (or the native validator); record dispositions Test completeness 100% for the top 20; their precision measured
Days 61–90 Shadow mode for all changes; first emulation of five priority techniques; health checks for them; first coverage report Validated coverage for five techniques; change failure rate tracked

Quick wins: visible exclusions and evidence-based tuning. Dependencies: SecDataOps (data), SOC Ops (dispositions), ThreatOps (priorities), platform owners (API tokens).

Pre-merge checklist (every pull request)

  • Request linked; threat, assets and data stated
  • Strategy document updated, blind spots included
  • At least one positive and one negative test; known gaps written as tests
  • Technique IDs checked against the current ATT&CK release
  • Filters have reason, owner and expiry
  • CI green: lint, tests, sigma check --fail-on-issues or native validator, conversion
  • Reviewer is not the author

Promotion checklist (staging to production)

  • Staging period completed; volume within the estimate; matches sampled with an analyst
  • Runbook linked
  • Severity agreed with the SOC lead
  • Health checks defined (rule runs, data presence, canary)
  • Release tag created by a maintainer on main

Monthly health review

  • Silent rules investigated (runs, data or logic)
  • Expired filters renewed with evidence or removed
  • Rules past their review date renewed or deprecated
  • Deployed-versus-repository drift is zero
  • Toolchain pins reviewed

Quarterly governance review

  • Coverage by implementation, with evidence and gap register, reviewed with risk owners; mappings current

Detection request (intake form)

Example status: template.

Request ID:        DR-____
Requested by:      (team, person)           Date:
Source:            intel report / hunt / incident / audit / SOC
Behaviour:         (plain words + ATT&CK technique or sub-technique)
Implementations:   (which ways of doing it must be detected)
Assets in scope:   (systems, criticality)
Data needed:       (log source, event IDs, fields; available? Y/N)
Evidence:          (hunt query, incident timeline, report link)
Expected volume:   (alerts per week, best guess)
Response:          (what the SOC should do; runbook exists? Y/N)
Priority:          (impact x likelihood, with reasons)
Decision:          accepted / parked (data gap) / rejected   By:      Date:

Filter record (one per exclusion)

Example status: template.

Filter ID:     FLT-____        Applies to rules:  (IDs)
Reason:        (alert samples, analyst evidence)
Logic:         (fields and values excluded)
Blind spot:    (what an attacker could hide behind this filter)
Known-gap test: (test file and case name)
Owner:         (person)        Expiry: YYYY-MM-DD   Reviewed by:

Retirement record

Example status: template.

Rule ID / title:   (id, title)               Status -> deprecated
Reason:            replaced / obsolete technique / data source retired /
                   cost exceeds value (precision, analyst minutes per TP)
Replacement:       (rule IDs, in Sigma "related" with type)
Coverage impact:   (implementations no longer detected, if any)
Approved by:       (detection lead)  SOC informed: (date)

Sigma, native queries, or a threat-to-rule model

Approach Strengths Limits Choose when
Native queries as code (KQL, SPL, ES|QL, YARA-L) Full platform features; no conversion Lock-in One platform, deep feature use
Sigma as source of truth Portable; community library; validators Conversion gaps; pipelines; toolchain churn Several platforms or tenants
Threat-to-rule model (e.g. OpenTide) Threat → objective → rule traceability[25] Another schema and tool Traceability is the main pain

Is DetectionOps the same as detection engineering? In practice yes: the label names detection engineering run as a DevOps-style loop[2]. Hire under the standard names.

Do we need Sigma? No. It helps with several platforms or community content; one platform can keep native queries as code with the same gates.

How many tests does a rule need? One that must fire and one that must not[27], a known-gap test per accepted blind spot, and a real regression sample when you can[43].

Our vendor claims 90% ATT&CK coverage. Is that coverage? It is a mapping. Ask which implementations, on which assets, at what precision, validated when[30].

Should detection content be open source? Generic rules often, yes; your filters, gaps and priorities, no, since they map your blind spots[6].

  • ATT&CK detection strategies (established since v18). v20 lists 703 Enterprise detection strategies and 1,770 analytics[29].
  • Coverage measurement (maturing). Summiting the Pyramid 5.0 (September 2026) added a Detection Coverage Calculator[15]; Sigma has an stp tag namespace for robustness scores[34].
  • Correlation as code (uneven support). Sigma correlation rules express counts, distinct values and temporal order[62]; test conversion per backend.
Stage Do this Output
Prerequisites Read SOC Ops and SecDataOps; learn Git and one query language You know where alerts come from
One day Read the Sigma specification[24] and the Pyramid of Pain[8]; do Lab 1 A linted rule
One week Lab 2; an ADS strategy for a rule you run; CTID’s quality and mapping pages[21][30] A tested rule with a known gap
30 days Lab 3 on a test SIEM; precision for your five noisiest rules; plan Lab 4 A pipeline and first metrics
Specialise Emulation engineering, coverage analytics, cloud or identity detection (CloudSecOps, IdentityOps) A portfolio repository with tests and a coverage report
Topic Remember
Definition Detection engineering run like software delivery; “DetectionOps” is a rare label for it
Unit of work A detection with requirement, data dependency, code, tests, owner, review date
Practices Strategy before query; test-first; review; staging; dispositions; health checks; emulation; retirement
Metrics Precision per rule; validated coverage; test completeness; rule health; latency
Watch-outs Tag-based coverage, console hot-fixes, unpinned toolchains, filters without expiry, silent rules

A judgement call: Sigma as the source of truth, or native queries as code?

Link to section “A judgement call: Sigma as the source of truth, or native queries as code?”

Situation (illustrative). A 1,500-person company runs Microsoft Sentinel; two engineers maintain about 150 KQL analytics in the portal. The CISO wants detection-as-code and has heard Sigma makes rules portable; a SIEM migration is “possible within three years”.

Option A: everything in Sigma, KQL generated. Attractive: portability, community rules, validators, a hedge against migration. Costs: every rule must survive conversion; KQL features the team relies on (watchlist joins, functions, enrichment) may not map; analysts debug queries they did not write; and the toolchain is in flux (pinning caution above).

Option B: KQL as the source of truth, as code. Deploy the analytics rules through Sentinel’s repository connection[40], with the same tests, review, staging and metrics. Microsoft now recommends custom detections for new rules[44]: a dated assumption (October 2026) to revisit, not a reason to rewrite 150 working analytics. Cost: lock-in; a migration means rewriting.

Deciding assumptions. Migration probability and timing; the share of rules using platform-specific features; whether you will share or consume Sigma content; capacity to own a conversion toolchain.

Reasoning. With one platform, two engineers and an uncertain migration, the expensive part of any migration is re-validating behaviour on new telemetry, not syntax. Requirements, tests and strategy documents survive it; generated queries help little if field semantics change. So Option B now, with cheap portability: keep requirements, strategies and test cases platform-neutral, and author in Sigma only simple single-event rules and community imports. Revisit if a migration is scheduled, a second platform appears, or a partner requires Sigma. “Sigma everywhere” optimises for a migration that may never come and pays daily in conversion gaps.

After this page you can explain the label, write a Sigma rule with positive, negative and known-gap tests, design a staged pipeline with scoped credentials, measure precision, coverage and rule health, and decide how much of the practice to adopt.

1. Why is “share of ATT&CK techniques with a tagged rule” a weak coverage measure?

A tag says nothing about which implementations are detected, how robust or precise the rule is, or whether it fired in a recent emulation. Use implementation coverage, detection quality and validation evidence instead.

2. In Lab 2 the filter raised precision from 0.88 to 1.00 and cut recall from 1.00 to 0.88. What should the team do?

Decide explicitly: keep the filter only if the updater’s benign positives cost more than the risk of malware at that exact path, with an owner, an expiry and the known-gap test. Better: narrow it with context an attacker cannot easily copy (such as the binary’s signer, if logged), or cover the gap with another rule.

3. A rule passed CI and staging but missed the behaviour in an emulation. Give two likely causes.

Fixtures unlike real telemetry (field names, encoding, mapping), or an implementation the rule never covered (PowerShell instead of schtasks.exe). Turn the run’s raw events into a regression sample.

4. Why does the Lab 3 validate job get no secrets, and what stops a writer reaching them anyway?

It runs unreviewed pull-request code. Tokens live only as environment secrets; staging accepts only main, production only maintainers’ release-* tags on main, approved by someone other than the author (plan permitting).

5. After an ATT&CK release your dashboard shows Defense Evasion coverage collapsing. Why, and what is the fix?

The release renamed or split the tactic (v19 did this), so hand-kept tactic tags no longer match. Detection did not change; derive tactics from technique IDs.

“DetectionOps” is a small label for a large idea: treat each detection as a tested, owned and measured product rather than a saved query. Tools matter less than that contract, above all tests for what you chose not to see. Keep it, and “what do we detect, and how do we know?” has an answer better than a heatmap.

Sources were opened on 2026-10-16 or 2026-10-17; survey figures come from summary pages, not full reports. Ledger: research/detectionops-ledger.md.

Changelog

  • 2026-10-16: first version (labs 1–2 executed, lab 3 syntax-checked); term status overlapping synonym: the label comes from one project, and the practice has better-known names.
  • 2026-10-17: revised after independent review: Lab 3 token reach and plan limits; Lab 2 evasion cases and an anchored filter (re-run); SigmaHQ starting points; Microsoft rule types; SOC Ops’ dispositions; rule-run and ingestion-delay checks; latency KPI; registry-only tasks as a known gap.
  • 2026-10-17, second review pass: rule B extended to event 4702 (TaskContentNew) with fixtures and Lab 4 implementation 6; \ProgramData\ and %TMP% in both rules, path lists declared a teaching subset; Labs 1–2 re-run; workflow paths covers itself; about 110 words of restatement cut.
  • 2026-10-17, post-score polish: benign-positive precision convention (Magonia); lab output ≤ 90 characters (Labs 1–2 re-run); tooling signpost; footnotes re-checked; CSF wording; restatements cut.

  1. OpenTideHQ (GitHub organisation) — OpenTide project, live page. Checked 2026-10-16. ↩︎ ↩︎

  2. When TI made actionable drives your Threat Detection (slides) — Hack.lu 2025, Amine Besson, Remi Seguy, Claus Houmann, 2025-10-23. Checked 2026-10-16. ↩︎ ↩︎

  3. 2025 SANS Detection Engineering Survey: Evolving Practices in Modern Security Operations — SANS Institute (summary page), 2025-02-24. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  4. Elastic releases the Detection Engineering Behavior Maturity Model — Elastic Security Labs, Mika Ayenson, Terrance DeJesus, Samir Bousseaden, 2024-09-06. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  5. Summiting the Pyramid: Detection Engineering Workflow — MITRE CTID. Checked 2026-10-16. ↩︎ ↩︎

  6. Alerting and Detection Strategies Framework — Palantir, 2017. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  7. The State of Detection Engineering Report 2026 — Anvilogic, sponsor’s summary of a SANS survey report, 2026. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  8. The Pyramid of Pain — David J. Bianco, Enterprise Detection & Response, 2013-03-01 (updated 2014-01-17). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  9. About Sigma — SigmaHQ documentation. Checked 2026-10-16. ↩︎

  10. SigmaHQ/sigma: main rule repository — SigmaHQ README. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  11. Detection Engineering Maturity Matrix — Kyle Bailey, 2021 (updated 2022-09). Checked 2026-10-16. ↩︎ ↩︎

  12. Taming the flood — European Commission Open Source Programme Office, 2025 (updated 2025-05-21). Checked 2026-10-16. ↩︎ ↩︎

  13. Unleashing the power of purple teaming with OpenTIDE — Hack.lu 2024 workshop, Remi Seguy and Amine Besson, 2024-10-23. Checked 2026-10-16. ↩︎

  14. OpenTIDE Workshop – Build Your Own Operational DetectionOps Platform in 90 Minutes — Hack.lu 2026, scheduled 2026-10-23. Checked 2026-10-16. ↩︎

  15. Summiting the Pyramid: Changelog — MITRE CTID (version 5.0, 2026-09-10). Checked 2026-10-16. ↩︎ ↩︎

  16. sigma-cli README — SigmaHQ. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  17. ATT&CK Detection Strategies, Analytics and Data Components — MITRE. Checked 2026-10-16. ↩︎

  18. DET0441: Detection of Suspicious Scheduled Task Creation and Execution on Windows — MITRE ATT&CK, modified 2026-05-12. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  19. Investigate incidents with Microsoft Sentinel (legacy) — Microsoft Learn, updated 2026-07-02 (closing classifications). Checked 2026-10-17. ↩︎

  20. Summiting the Pyramid: Definitions — MITRE Center for Threat-Informed Defense. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  21. Summiting the Pyramid: Detection Quality — MITRE CTID. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  22. Summiting the Pyramid: Implementation Coverage — MITRE CTID. Checked 2026-10-16. ↩︎ ↩︎

  23. Detection Engineering (poster) — SANS Institute, Kathryn Hedley and Nick Mitropoulos, 2026-02-09. Checked 2026-10-16. ↩︎ ↩︎

  24. Sigma Rules Specification, version 2.1.0 — SigmaHQ, 2025-08-02. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  25. opentide · spec-driven development for detection engineering — OpenTide project (v0.15.0 banner). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  26. SigmaHQ rules Schedule Task Creation From Env Variable Or Potentially Suspicious Path Via Schtasks.EXE (modified 2025-10-07) and Suspicious Scheduled Task Creation — SigmaHQ rules repository. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  27. Testing (detections) — Panther documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  28. Sigma Filters Specification, version 2.1.0 — SigmaHQ, 2025-08-02. Checked 2026-10-16. ↩︎ ↩︎

  29. ATT&CK Updates – October 2026 (v20) — MITRE, 2026-10-13. Checked 2026-10-16. ↩︎ ↩︎

  30. Summiting the Pyramid: Mapping Detections to ATT&CK — MITRE CTID. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  31. ATT&CK Updates – October 2025 (v18) — MITRE, 2025-10-28. Checked 2026-10-16. ↩︎

  32. ATT&CK Updates – April 2026 (v19) — MITRE, 2026-04-28. Checked 2026-10-16. ↩︎

  33. ATT&CK Enterprise Tactics — MITRE (v20). Checked 2026-10-16. ↩︎ ↩︎

  34. Sigma Tags Appendix — SigmaHQ (history to v2.1.0, 2025-08-02). Checked 2026-10-16. ↩︎ ↩︎

  35. SigmaHQ rule file carrying the attack.stealth tag — SigmaHQ rules repository (file modified 2025-02-23). Checked 2026-10-16. ↩︎

  36. Single and multiple event rules in YARA-L — Google Security Operations documentation, updated 2026-10-13. Checked 2026-10-17. ↩︎

  37. sigma-cli on PyPI (3.1.0, 2026-07-07), pySigma (2.0.1, 2026-10-11), pySigma-backend-splunk (2.1.0), pySigma-pipeline-windows (2.0.1) — Python Package Index. Checked 2026-10-17. ↩︎ ↩︎

  38. elastic/detection-rules — Elastic README. Checked 2026-10-16. ↩︎

  39. DaC Reference: Managing Elastic Security Detection Rules Using DaC — Elastic (version 0.4.0). Checked 2026-10-17. ↩︎

  40. Deploy content as code from your repository — Microsoft Learn, updated 2026-07-01. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  41. splunk/contentctl — Splunk Threat Research Team README. Checked 2026-10-16. ↩︎ ↩︎

  42. Atomic Red Team — Red Canary README. Checked 2026-10-16. ↩︎

  43. Sigma Regression Testing — SigmaHQ README. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  44. Handle ingestion delay in Microsoft Sentinel — Microsoft Learn, updated 2026-07-02. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  45. Scheduled Task, T1053.005 — MITRE ATT&CK, technique version 1.8. Checked 2026-10-16. ↩︎ ↩︎

  46. Sigma Modifiers Appendix — SigmaHQ (windash). Checked 2026-10-17. ↩︎

  47. 4698(S): A scheduled task was created — Microsoft Learn, 2021-09-07. Checked 2026-10-16. ↩︎ ↩︎

  48. 4702(S): A scheduled task was updated — Microsoft Learn, 2021-09-07 (field TaskContentNew). Checked 2026-10-17. ↩︎ ↩︎

  49. GhostTask README — netero1010, GitHub (last push 2025-01-02). Checked 2026-10-17. ↩︎

  50. actions/checkout, actions/setup-python, actions/upload-artifact and actions/download-artifact — GitHub, live READMEs (v7, v7, v7, v8). Checked 2026-10-16. ↩︎

  51. sigma-cli source: check command — SigmaHQ, main branch. Checked 2026-10-16. ↩︎

  52. pySigma-pipeline-windows — SigmaHQ, source on the main branch (pipeline windows-logsources). Checked 2026-10-17. ↩︎

  53. Deployments and environments — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  54. About rulesets and Available rules for rulesets — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎

  55. Managing environments for deployment — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎

  56. Atomic Red Team website — Red Canary. Checked 2026-10-16. ↩︎

  57. Execute Atomic Tests (Local) — Invoke-AtomicRedTeam wiki, edited 2023-09-18. Checked 2026-10-16. ↩︎

  58. What Framing Security Alerts as a Binary True or False Positive is Costing You — Magonia Research, signalblur, 2025-08-07. Checked 2026-10-17. ↩︎

  59. The NIST Cybersecurity Framework (CSF) 2.0, NIST CSWP 29 — NIST, 2024-02-26. Checked 2026-10-16. ↩︎

  60. Sysmon v15.22 — Microsoft Sysinternals, updated 2026-09-10. Checked 2026-10-16. ↩︎

  61. Monitor the health and audit the integrity of your Microsoft Sentinel analytics rules — Microsoft Learn, updated 2026-07-02. Checked 2026-10-17. ↩︎ ↩︎

  62. Sigma Correlation Rules Specification — SigmaHQ. Checked 2026-10-16. ↩︎

Related Ops

ThreatOps

ThreatOps is an unstandardised label. This page uses it for running threat intelligence and threat hunting as one operation: priority intelligence requirements steer what is collected and analysed, and testable hypotheses steer hunts whose findings change defences.

SOC Ops

SOC Ops is running a security operations centre as a dependable service: turning alerts into timely, recorded decisions through tiers, playbooks, SIEM and SOAR, shift handover, metrics and sourcing choices that people can sustain around the clock.

SecDataOps

SecDataOps is the engineering and governance of security telemetry: onboarding log sources, parsing and normalising them to a schema, enriching, routing and tiering them, and retaining them so that detections, investigations and audits have complete, trustworthy and affordable evidence.

AIOps for Security

AIOps for Security is the use of statistics, machine learning and language models inside the detect-triage-respond loop of a security operation, evaluated and governed against an adversary: base rates, missed detections, prompt injection and limits on autonomy.

ResponseOps

In this Atlas, ResponseOps is security incident response run as an operational capability: contain an active adversary, preserve evidence, evict and recover cleanly, and meet notification duties. The label itself is not a standard term.

DevSecOps

DevSecOps is the practice of building security into the software delivery pipeline and into the teams that run it, so that every change is checked, traceable and safe to ship without a separate security phase that stalls delivery.

SecOps

SecOps is an ambiguous label. Here it means the working contract between security and IT operations: agreed hardening baselines, one owned remediation queue with deadlines, governed exceptions and joint metrics. Others use it for SOC work or a Google product.

AIOps

AIOps (artificial intelligence for IT operations) applies statistics, machine learning and language models to operational events and telemetry to cut alert noise, group related alerts into incidents, flag anomalies, rank probable causes and run guarded fixes.

Practise and get certified

From xOps School

Practise DetectionOps and get certified

The free DetectionOps track on xOps School has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-17 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)