Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
0.5 Four Ways to Write a Detection
Introduction
Every detection in this course is written four times, and always in the same order: Sentinel KQL, Splunk SPL, PowerShell with Get-WinEvent, and Sigma. That isn't repetition for its own sake. The four forms are four places an Active Directory event can be read, and the place you read it from depends on where you work, what your organization bought, and whether the SIEM got the event at all.
This lesson takes one simple detection, a member added to a security group on a domain controller, and writes it in all four, so that every later lesson's detection block reads as one rule in four languages rather than four things to learn.
The rule is deliberately small. A group membership change is one of the most common things a controller records and one of the most useful, because Active Directory grants nearly everything through groups. In the month this course reads, the writable controllers recorded six such changes, and the lesson finds them each way:
The figure is the reason there are four surfaces. One administrator makes one change, the controller writes one event, and that record is then read in three different places: a Sentinel workspace and a Splunk index, each fed by an agent, and the controller's own log, read directly with PowerShell.
Sigma sits beside them as a description of the rule that compiles into whichever language you need. By the end you'll have run the rule in KQL and SPL, read its PowerShell output, and seen the Sigma rule converted.
One Rule, Four Places to Run It
understand: what a surface isA surface, in this course, is a place a detection runs and the language it is written in there. The course fixes four of them and an order, so a lesson's detection block always opens on the same tab and a student who works in one product always knows where to look:
The four surfaces, in the order every lesson shows them
spec D5The order follows the market rather than a preference. Microsoft Sentinel and Splunk are the two SIEMs a Windows security team is most likely to have, and the course's corpus is projected into both, so every query you see in KQL or SPL runs in the page against the same month of events.
PowerShell comes third because it needs nothing but a controller and permission to read its log, which makes it the surface for a small estate with no SIEM and for an incident in which the SIEM is the thing in doubt. Sigma comes last because it is a description rather than a query: it says which log and which conditions, and a converter writes the query.
The Event Every Surface Reads
observe: event 4728 and its fieldsBefore writing a rule in any language, read the event it's built on in the form the controller wrote it. Event 4728 records a member added to a security-enabled global group, and it is written only by domain controllers, under the Audit Security Group Management subcategory. The first one of the month, read from NE-DC01's exported account management events, field by field:
PS C:\> Get-WinEvent -ComputerName NE-DC01 -FilterHashtable @{ LogName = "Security"; Id = 4728 } -MaxEvents 1 -Oldest |
>> ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data } | Format-Table Name, '#text' -AutoSize
Name #text
---- -----
SubjectUserSid S-1-5-21-3623811015-3361044348-30300820-1587
SubjectUserName a-mwebb
SubjectDomainName NE
TargetUserName Finance-Share-RW
TargetDomainName NE
MemberName CN=r.crompton,OU=Staff,DC=corp,DC=ne,DC=com
MemberSid S-1-5-21-3623811015-3361044348-30300820-1541
Seven fields, and three of them carry the whole meaning. SubjectUserName is who made the change, a-mwebb, one of Northgate's tier 0 administrators. TargetUserName is the group that changed, Finance-Share-RW. MemberName is the account that was added, written as a distinguished name, which tells you r.crompton sits in the Staff organizational unit.
The two SIDs identify the same people by number, and a SID is the safer thing to match on when names can be reused, but the names are what an analyst reads first and what the course's rules match.
Group scope decides the event number. A global group's change is 4728, a domain local or builtin group's is 4732, and a universal group's is 4756, and the fields are the same in all three.
A rule that reads only 4728 misses a change to Administrators, which is a builtin domain local group, so the rule in this lesson reads all three numbers even though the month holds only 4728. The subcategory also writes no failure events, so a membership change that was refused leaves nothing for any of the four surfaces to read.
What the event doesn't say matters as much as what it does. It names the group and nothing about what membership of that group grants.
Finance-Share-RW reads like a share permission, and it probably is one, but the only way to know is to look at the places the group is used: the share's permissions, a GPO that adds it to a local Administrators group, an ACL on a directory object.
Two events that differ only in the group's name can mean a routine request and a takeover, which is why every narrowed rule in the course starts from a list of groups whose grants have been read, rather than from the event alone.
Each surface names those fields its own way, and most of the difference sits in one place:
KQL, PowerShell and Sigma keep the event's own field names, because the Sentinel table and the event XML both carry them through. The Splunk index renames them at search time, so TargetUserName becomes Group_Name and SubjectUserName becomes src_user. Neither naming is wrong.
The trap is a query translated by hand from one to the other, which returns nothing without any error when a single field name is left in the wrong language.
In Sentinel: KQL
detect: the SecurityEvent tableSentinel stores Windows security events in the SecurityEvent table, one row per event, with the event's fields as columns. The rule is a filter on the event number and the controller, then a projection that renames the three fields that matter into words an analyst reads at a glance:
SecurityEvent
| where EventID in (4728, 4732, 4756) and Computer startswith "NE-DC"
| project TimeGenerated, Computer, EventID, Group = TargetUserName, Member = MemberName, Operator = SubjectUserName
| sort by TimeGenerated asc
Run it, and six rows come back, every one a 4728 from NE-DC01 and every one by a tier 0 administrator. The where line does all the selecting. EventID in (4728, 4732, 4756) reads all three group scopes, and Computer startswith "NE-DC" keeps the two writable controllers, because Computer holds the full DNS name. NE-RODC01, the read-only controller, is left out on purpose.
A read-only controller holds no writable copy of the domain, so a membership change made through it is passed to a writable controller, and the 4728 is written there. Reading the read-only controller for group changes costs a query and can never return a row. The project line is presentation: it renames TargetUserName, MemberName and SubjectUserName to Group, Member and Operator, so the result reads as a sentence.
Six rows over a month is a small number, and it's worth seeing what they are before deciding what the rule is for:
All six are additions to groups that open a file share or the VPN, made by the two people whose job it is. A detection that fires on them is working exactly as written, and none of its rows needs a response.
That is the honest description of the simple form: it is an inventory of membership changes, useful for a monthly review and as the base for a sharper rule, and it is not yet an alert. The last section of this lesson shows how far the course's own rule narrows it, and lesson 11.3 builds that narrower rule from an attack that needs it.
In Splunk: SPL
detect: the same rows, different namesIn Splunk the same events sit in the Windows event log index, and the rule has the same three parts: select by event code and host, then lay out and rename the fields. Run it against the course's Splunk projection of the same month:
index=wineventlog sourcetype="WinEventLog:Security" EventCode IN (4728, 4732, 4756) host="NE-DC*"
| table _time, host, EventCode, Group_Name, Member_Name, src_user
| rename Group_Name AS Group, Member_Name AS Member, src_user AS Operator
| sort _time
The same six rows, at the same times, for the same groups and operators. The search reads differently because Splunk's names are different, and the card below sets the two languages against each other field by field, which is the comparison every later lesson's detection block relies on:
The same rule, read field by field
KQL and SPL on the course corpusTwo differences deserve attention beyond the names. Splunk matches host with a wildcard pattern where KQL uses startswith, and the two agree only because every controller name begins with NE-DC; a host field holding a short name in one estate and a full DNS name in another changes what the pattern matches.
The second is the sourcetype. This course's index files the Security log as WinEventLog:Security, an older convention. Splunk's Windows add-on, from version 5.0.0, assigns sourcetype WinEventLog to classic channels and XmlWinEventLog to XML ones and keeps the channel in the source field, so check which form your own index uses before you paste a search in.
That agreement is the rule in the last row of the first card, and the course holds every detection to it. A detection that returns six rows in one SIEM and five in the other has a translation fault in it, usually a field name, and finding that fault on a quiet month is far cheaper than finding it during an incident.
The generator that writes this lesson runs both queries against the corpus and stops if their rows differ by time, group or operator, and every detection block from Module 1 onward has passed the same check. When you write your own detections in two languages, the same comparison is the quickest test you have.
With No SIEM: PowerShell
detect: reading the controller directlyPowerShell's surface reads the event where it was written. Get-WinEvent takes a filter hashtable that names the log and the event numbers, so the controller does the filtering and returns only the matching records; the loop then turns each record's XML into an object with the same three fields the SIEM rules project. Shown as typed on an analyst workstation, with its output produced from NE-DC01's exported account management events:
PS C:\> $ev = foreach ($dc in "NE-DC01", "NE-DC02") {
>> Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = "Security"; Id = 4728, 4732, 4756 } | ForEach-Object {
>> $d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.InnerText }
>> [pscustomobject]@{ Time = $_.TimeCreated.ToUniversalTime(); Controller = $dc; Id = $_.Id; Group = $d.TargetUserName; Member = $d.MemberName; Operator = $d.SubjectUserName } } }
>> $ev | Sort-Object Time | Select-Object @{ n = "Time"; e = { $_.Time.ToString("yyyy-MM-dd HH:mm:ss") } }, Controller, Id, Group,
>> @{ n = "Member"; e = { ($_.Member -split ",")[0] -replace "^CN=" } }, Operator | Format-Table -AutoSize
Time Controller Id Group Member Operator
---- ---------- -- ----- ------ --------
2026-02-18 09:32:03 NE-DC01 4728 Finance-Share-RW r.crompton a-mwebb
2026-02-19 14:31:45 NE-DC01 4728 VPN-Users e.bevan a-pgreaves
2026-02-20 13:05:57 NE-DC01 4728 Engineering-Share-RW q.fenwick a-mwebb
2026-02-24 16:20:50 NE-DC01 4728 VPN-Users a.davis a-pgreaves
2026-03-10 16:29:41 NE-DC01 4728 Finance-Share-RW h.crompton a-pgreaves
2026-03-11 11:47:45 NE-DC01 4728 VPN-Users i.jarvis a-pgreaves
The same six rows again, now with the member shortened to its account name, because the distinguished name's first part is the useful one at a terminal. The export holds eighteen events, the six group changes and the twelve password resets NE-DC01 recorded, and the filter returned only the six, which is the filter hashtable doing what the where line did in KQL.
Where the filter runs matters. The filter hashtable is handed to the event log service on the controller, which returns only the matching records, so the rule moves six events across the network rather than every Security event the controller holds.
The same rule written as Get-WinEvent piped to Where-Object would read the whole log first and discard almost all of it, which on a controller writing thousands of events an hour turns a query of seconds into one of minutes, and the results are identical either way.
When a lesson shows a PowerShell detection, the filtering that the log service can do is always in the hashtable, and Get-WinEvent offers more than one way to put it there:
Where Get-WinEvent does its filtering
Microsoft's Get-WinEvent referenceThe PowerShell form has limits the SIEM forms don't. Get-WinEvent is available only on Windows, and its -ComputerName parameter takes one computer at a time, so the rule loops over the controllers by name, and it needs the event log service reachable through each controller's firewall. It also reads only what the log still holds, and a busy controller's Security log can roll over in days.
Those limits are also its strength. It needs no agent, no workspace and no index, so it works on the day a team discovers the controllers' events were never shipped, and it works on an exported file from a domain you don't administer, which is how this course's evidence reaches you.
An estate with many controllers and no SIEM usually adds Windows Event Forwarding, which forwards the events you choose from every controller to a Windows Event Collector server that logs them locally, and then the same rule runs once against the collector's log instead of looping over the controllers.
Written Once: Sigma
detect: a rule that compilesSigma describes the rule rather than searching for it. A Sigma rule is a YAML file with a log source, made of a product, a service and sometimes a category, a detection section of named selections, and a condition that combines them. The rule for this lesson names the Windows Security log and the three event numbers, and nothing else:
title: Member Added to a Security Group on a Domain Controller
id: 9e2d4b71-3a6c-4f05-8b1e-6c7a0d3f2e58
status: experimental
description: A domain controller recorded a member added to a security-enabled group, global (4728), domain local or builtin (4732) or universal (4756). The simple form of the rule, with no watched list and no exclusions, shown in lesson 0.5 of Defending Active Directory beside its KQL, SPL and PowerShell forms; lesson 11.3's ADS-DET-1104 narrows it to the groups that grant access and the operators outside tier 0.
references:
- https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/audit-security-group-management
- https://attack.mitre.org/techniques/T1098/007/
author: Ridgeline Cyber
date: 2026-10-11
tags:
- attack.persistence
- attack.privilege-escalation
- attack.t1098.007
logsource:
product: windows
service: security
detection:
selection:
EventID:
- 4728
- 4732
- 4756
condition: selection
falsepositives:
- Every routine membership change an administrator makes; on its own this rule is an inventory of changes, not an alert
level: low
The rule carries its own metadata: the ATT&CK technique it detects, T1098.007, Additional Local or Domain Groups, the references it rests on, and a false positive note that says plainly what this simple form is. That metadata travels with the rule into whichever repository you keep detections in, which is a large part of why teams adopt Sigma for detections they maintain over years.
Two fields describe the rule's state rather than its logic. Status experimental says the rule hasn't yet been through a team's testing, and level low says that a match, on its own, is information rather than an incident; the narrowed rule in lesson 11.3 carries level high, because every row it returns is a change nobody approved.
The id is a fixed identifier, so a rule renamed or rewritten over the years is still recognizably the same rule in a SIEM's alert history. Converting it for Splunk with sigma-cli and the Splunk Windows pipeline:
PS C:\Detections> sigma convert -t splunk -p splunk_windows .\ads-det-005-member-added-to-security-group.yml
Parsing Sigma rules
source="WinEventLog:Security" EventCode IN (4728, 4732, 4756)
The converter turns the log source into source="WinEventLog:Security" and the selection into EventCode IN (4728, 4732, 4756). Pasted into the course's Splunk index it returns nothing, because this index names the Security log in sourcetype and carries no source field; change that one word and the search finds the same six events.
That is the conversion working as designed: the pipeline encodes one estate's conventions, and yours decide whether they fit. The output also shows what Sigma leaves to you. There is no index, no restriction to controllers, no table and no renaming, because those depend on your estate rather than the rule.
A converted Sigma rule is a starting search, and the course's Sigma files are written to convert cleanly with exactly this command, so the step from rule to working search is small.
Why the Course Keeps All Four
decide: which surface you reach forWriting every detection four times costs the course effort, and it's worth saying what that buys you. The answer is that the surface you use isn't your choice alone: your employer's SIEM chooses the first, an incident can take it away, and a detection you write today may need to run somewhere else next year. The decision as a practitioner meets it:
The first two branches are where most readers stop, and the course writes KQL and SPL with equal care so that neither reader is translating. The third branch is Sigma's reason to exist: a team on Elastic, QRadar or Chronicle converts the rule with that SIEM's backend instead of rewriting it.
The last branch matters more than its position suggests. An investigation often begins with the discovery that the controllers' events never reached the SIEM, or that the SIEM's retention ended before the intrusion did, and then PowerShell on the controllers or on an exported log is the only surface left.
Each surface also has a cost the course designs around, and knowing them is part of choosing well:
What each surface costs you
the trade-offs the course works aroundThe last row is the one exception to four surfaces. Some evidence exists only in Defender for Identity's own tables, which record directory queries and authentication as its sensors see them, and has no Windows event behind it. Those detections are written in KQL and SPL only, and the lesson that writes one says so, rather than inventing a PowerShell or Sigma form that would read something else.
Most rules are written four times; the few that compare a month against a baseline or read the directory's own state use the surfaces that can do that, and their entries say why. The course's detection pack in lesson 19.1 ships each rule in every surface its source allows.
From a Simple Rule to the Course's Rule
verify: what narrowing removesA simple rule is the right place to learn the surfaces and the wrong place to stop. Before narrowing it, practice reading it a level up: count the month's changes by group, which is the summary a monthly review starts from.
Three groups, and not one of them grants administration. That observation is the whole of the next step.
The course's own version of this rule, built in lesson 11.3 from an attack that abuses group membership, adds two conditions: the group must be on a watched list of the groups that grant something, and the operator must be someone other than a tier 0 administrator. The month seen through each condition in turn:
The funnel runs from every Security event the three controllers recorded to the rows a person should look at, and the narrowed rule returns none of the six, because each was a routine change by the right people to a group that grants nothing dangerous. That empty result is not a failure.
A detection that is silent through a quiet month and fires on the change nobody approved is the goal of every detection in this course, and the lessons ahead each start from a simple form like this one, prove it in KQL and SPL against the corpus, and narrow it until its rows are the ones worth a person's time.
The finished rule, as every lesson's detection block will show it:
SecurityEvent
| where EventID in (4728, 4732, 4756) and Computer startswith "NE-DC"
| project TimeGenerated, Computer, EventID, Group = TargetUserName, Member = MemberName, Operator = SubjectUserName
| sort by TimeGenerated asc
index=wineventlog sourcetype="WinEventLog:Security" EventCode IN (4728, 4732, 4756) host="NE-DC*"
| table _time, host, EventCode, Group_Name, Member_Name, src_user
| rename Group_Name AS Group, Member_Name AS Member, src_user AS Operator
| sort _time
$ev = foreach ($dc in "NE-DC01", "NE-DC02") {
Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = "Security"; Id = 4728, 4732, 4756 } | ForEach-Object {
$d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.InnerText }
[pscustomobject]@{ Time = $_.TimeCreated.ToUniversalTime(); Controller = $dc; Id = $_.Id; Group = $d.TargetUserName; Member = $d.MemberName; Operator = $d.SubjectUserName } } }
$ev | Sort-Object Time | Select-Object @{ n = "Time"; e = { $_.Time.ToString("yyyy-MM-dd HH:mm:ss") } }, Controller, Id, Group,
@{ n = "Member"; e = { ($_.Member -split ",")[0] -replace "^CN=" } }, Operator | Format-Table -AutoSize
title: Member Added to a Security Group on a Domain Controller
id: 9e2d4b71-3a6c-4f05-8b1e-6c7a0d3f2e58
status: experimental
description: A domain controller recorded a member added to a security-enabled group, global (4728), domain local or builtin (4732) or universal (4756). The simple form of the rule, with no watched list and no exclusions, shown in lesson 0.5 of Defending Active Directory beside its KQL, SPL and PowerShell forms; lesson 11.3's ADS-DET-1104 narrows it to the groups that grant access and the operators outside tier 0.
references:
- https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/audit-security-group-management
- https://attack.mitre.org/techniques/T1098/007/
author: Ridgeline Cyber
date: 2026-10-11
tags:
- attack.persistence
- attack.privilege-escalation
- attack.t1098.007
logsource:
product: windows
service: security
detection:
selection:
EventID:
- 4728
- 4732
- 4756
condition: selection
falsepositives:
- Every routine membership change an administrator makes; on its own this rule is an inventory of changes, not an alert
level: low
Four tabs, one rule. From Module 1 onward the block looks like this in every attack lesson, opening on KQL, and the rows each tab returns on the corpus are the rows the lesson's prose describes.
Practice
- Find the event. Read one 4728 from a controller's Security log with Get-WinEvent and note the five fields in this lesson's figure.
- Write it in your SIEM. Run the KQL or the SPL form against your own data for the last 30 days and count the rows.
- Write it in the other one. If your estate has only one SIEM, run the same rule as PowerShell on a controller and compare the rows with your SIEM's.
- Convert the Sigma rule. Download the rule, convert it with sigma-cli for your SIEM, and add whatever the converter leaves out: index, table, sort.
- Read the result. Every row is a change somebody made. For each, name who approved it; a row nobody can name is the start of lesson 11.3.
Next, lesson 0.6: what the course builds, module by module, and the tools, resources and project you'll finish with.