In this section

0.3 The Evidence This Course Reads

Module 0

Introduction

Every claim this course makes about Northgate's domain comes from somewhere you can open. This lesson is the map of those places: what the course reads, how much of it there is, what writes each part, and how a lesson tells you which part a fact came from.

Read it once now, and come back to it the first time a lesson shows you a number and you want to know whether to trust it.

Three kinds of evidence, one domain What a lesson reads about corp.ne.com The AD corpus events, 14 Feb to 15 Mar Evidence files directory state, a folder per module Modeled evidence after 15 March, labeled SecurityEvent, Event Windows logs Three Identity tables Defender for Identity SecurityAlert two alerts ads00 free with this module ads01 to ads24 for subscribers Exports and logs dated after the month Change records the course's own work Events come from the corpus; state comes from files; anything after the month is modeled and says so.

The tree has three branches, and the difference between them matters more than their size. The AD corpus holds events: things that happened in Northgate's domain during one month, recorded by the domain controllers, the certificate authority, the workstations and Defender for Identity.

The evidence files hold state: what the directory looked like when someone exported it, a user's attributes, a template's permissions, a trust's settings. And everything dated after the corpus month is modeled: the course's own work on the domain, its changes and tests, written down in evidence files and labeled so you always know you are reading one.

1

The Corpus Month

investigate: where the events start and stop

The corpus is a fixed month of Northgate's logs, and its edges are worth knowing exactly, because a query that runs past them returns nothing and a reader who doesn't know why will blame the query. Ask the Security events where each machine starts and stops, in either language:

SecurityEvent
| summarize Events = count(), First = min(TimeGenerated), Last = max(TimeGenerated) by Computer
| sort by Events desc
index=wineventlog sourcetype="WinEventLog:Security"
| stats count AS Events, min(_time) AS First, max(_time) AS Last BY host
| sort - Events

Three machines. NE-DC01 recorded 14,752 Security events and NE-DC02 7,125, from the first minutes of 14 February to the morning of 15 March; NE-ADCS01, the certificate authority, recorded 174 from 16 February to 13 March, all of them one event, a certificate issued.

Across all six tables the first row is at 00:00:29 on 14 February and the last at 11:53:22 on 15 March, both UTC. That last timestamp is the edge of everything the course can query, and every lesson treats it that way.

The month is fixed rather than live, and that is a decision with a reason. The corpus is generated from a fixed random stream for each scenario, so a count a lesson quotes today is the count it will return next year, and adding a new scenario for a later module can't move a figure an earlier lesson printed.

A live feed would drift under the lessons the day after they were written. The cost is that the month never grows, which is why the course needs the third branch of the tree.

The corpus month, and the modeled months after it 14 Feb 00:00 The corpus begins the first row, on NE-DC02 15 Mar 11:53 The corpus ends the last row in any table everything below is modeled 16 Mar Module 2 an audit test, modeled 23 Mar lesson 11.5 an evidence file, modeled 13 Apr lesson 13.4 a delegation report, modeled 27 Apr lesson 15.1 an evidence file, modeled 18 May lesson 18.1 an evidence file, modeled 15 Jun lesson 22.1 an evidence file, modeled 3 Jul lesson 24.3 the last export, modeled Four weeks of events, then four months of work the modules do, each dated and each read from an evidence file.

After the month, the modules keep working on the domain: Module 2 tests its audit policy on 16 March, Module 13 runs a delegation report on 13 April, and the last export the course reads, in lesson 24.3, is dated 3 July.

None of that is in the corpus, because the corpus ended. Each is in an evidence file, dated, and the lesson that reads it says it is modeled. The course runs on one calendar, so a change a lesson makes in April is still in place when a later lesson reads the domain in June.

2

Six Tables

investigate: what writes each one

The corpus is laid out the way Microsoft Sentinel and Defender XDR lay out the same data, so the queries you write here are the queries you would write at work. Six tables, each written by a different source:

The six tables, and what writes each

Microsoft's table references for Azure Monitor and Defender XDR; the corpus's Splunk projection

SecurityEvent

the Security log of NE-DC01, NE-DC02 and NE-ADCS01, 22,051 rows; in Splunk, index=wineventlog sourcetype=WinEventLog:Security

Event

every other Windows log collected: Sysmon from 54 machines, and System, Directory Service and Application from the controllers and a few servers, 14,641 rows; in Splunk, Sysmon under index=endpoint and the rest under index=wineventlog

IdentityLogonEvents

Defender for Identity's record of authentication at the controllers, 1,924 rows

IdentityDirectoryEvents

Defender for Identity's directory activity, here replication, 178 rows

IdentityQueryEvents

queries against the directory that Defender for Identity saw, LDAP and SAMR, 131 rows

SecurityAlert

alerts raised in the month, 2; in Splunk the three Identity tables are index=defender sourcetype=ms:defender:eventhub, and the alerts are not projected

Two of the six hold almost everything. SecurityEvent is the controllers' Security logs, which record authentication, directory access and changes, and it is the table most lessons start from. Event holds the other Windows logs, and most of it is Sysmon from 54 machines: the 49 laptops, the two writable controllers, a helpdesk workstation and two member servers, recording what ran and what it connected to.

The three Identity tables come from Defender for Identity, whose sensor on each controller reads the traffic and the events and writes its own view. Microsoft's reference describes IdentityLogonEvents as authentication activity on premises captured by Defender for Identity, and that is exactly what this table holds: 1,924 successful logons at NE-DC01 and NE-DC02. Sizes side by side:

Rows in each table of the AD corpus SecurityEvent controllers and the CA 22,051 Event Sysmon, System, Directory Service 14,641 IdentityLogonEvents Defender for Identity 1,924 IdentityDirectoryEvents Defender for Identity 178 IdentityQueryEvents Defender for Identity 131 SecurityAlert alerts 2 Bar length follows log10(count), so 2 stays visible beside 22,051. 38,927 rows in all, and more than half of them are the controllers' Security logs.

The log scale matters for reading the chart. SecurityAlert holds 2 rows and SecurityEvent 22,051, a ratio a straight bar could not show, and the ratio is the point: alerts are a handful of conclusions drawn from tens of thousands of events, and this course is mostly about reading the events. The two alerts, on 07 and 14 March, are read in the modules about the attacks they describe.

Every event is also in Splunk. The corpus carries a projection of the same rows into Splunk's sourcetypes, with field names Splunk's add-ons use: 22,051 rows of WinEventLog:Security, 14,418 of Sysmon, and the three Identity tables as ms:defender:eventhub.

That is why every query block in the course has a KQL tab and an SPL tab that return the same rows. The alerts are the one table not projected, because they are Sentinel's conclusions rather than a log. Where each table lives in Splunk:

The same events in Splunk

The corpus projection, with field names from the Splunk add-ons

SecurityEvent

index=wineventlog sourcetype=WinEventLog:Security, 22,051 events; TargetUserName is Account_Name, Status is Result_Code

Event, Sysmon rows

index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational, 14,418 events

Event, other logs

index=wineventlog, sourcetypes WinEventLog:System, :Directory Service and :Application, 223 events

The Identity tables

index=defender sourcetype=ms:defender:eventhub, 2,233 events

SecurityAlert

not projected: an alert is a conclusion, and Splunk raises its own

Every SPL search in the course starts with an index and a sourcetype, index=wineventlog with WinEventLog:Security for the controllers' Security events, index=endpoint for Sysmon, index=defender for the Identity tables, because Splunk narrows to those before it reads a single event, and a search that names neither reads everything it can see.

The KQL side has the same habit in another form: a query starts by naming its table, and a filter on time or EventID comes next, so the engine discards rows as early as it can.

3

What the Controllers Record

investigate: the Security events by ID

A Windows Security event is identified by its number, and the controllers wrote thirteen different ones in the month. Counting them is the quickest way to learn what the corpus can answer, and the same count works in both languages:

SecurityEvent
| summarize Events = count(), Computers = dcount(Computer) by EventID, Activity
| sort by Events desc
index=wineventlog sourcetype="WinEventLog:Security"
| stats count AS Events, dc(host) AS Computers BY EventCode, signature
| sort - Events

Service ticket requests, event 4769, are the largest at 11,602: every time an account reaches a file share, a database or a web application with Kerberos, the controller records one.

Ticket-granting ticket requests, 4768, follow at 4,121, one per account per sign-in, which lesson 0.4 counts. Then directory access, 4662, at 2,309, recorded when an object with an audit entry is read in a way the entry names; network logons to the controllers, 4624, at 2,093; and file share access, 5145, at 1,289, all but 2 of it reads of SYSVOL, the share Group Policy is delivered from.

The smaller rows are smaller because they record rarer things, and rare is where many of the course's findings start. NTLM validation, 4776, 202; certificates issued, 4887, 174, all on the certificate authority; special privileges at logon, 4672, 99; failed pre-authentication, 4771, 84; directory changes, 5136, 59; password resets, 4724, 12; members added to a security group, 4728, 6; and one group membership enumeration, 4799.

None of these appear because Windows writes them by default. Each belongs to a subcategory of the advanced audit policy, set by Group Policy on the controllers, and a subcategory left off means its events are never written:

The audit setting behind each event

Microsoft's Advanced Audit Policy subcategory references

4768, 4771

Audit Kerberos Authentication Service: ticket-granting tickets issued, and pre-authentication failures

4769

Audit Kerberos Service Ticket Operations: service tickets

4776

Audit Credential Validation: NTLM validated by the controller

4624, 4672

Audit Logon and Audit Special Logon

4662

Audit Directory Service Access, and an audit entry (SACL) on the object read

5136

Audit Directory Service Changes

5145

Audit Detailed File Share, which Microsoft warns is high volume on a controller, especially for SYSVOL

4724, 4728, 4799

Audit User Account Management and Audit Security Group Management

4887

Audit Certification Services, on the certificate authority

The card explains two of the counts above. File share access is the fifth largest event in the month because Northgate audits it on the controllers, and Microsoft's own guidance on that subcategory warns that success auditing there is high volume, especially for SYSVOL, which is where 1,287 of the 1,289 came from.

And 4662 is as large as it is because directory access is only recorded for objects that carry an audit entry, so its count is a measure of what Northgate chose to watch rather than of how busy the directory was. Module 2 teaches each of these settings, because an event that isn't audited is a question the log can never answer.

4

Reading One Event

investigate: a row, field by field

A row in SecurityEvent is a Windows event with its fields flattened into columns, and the fastest way to understand the table is to read one row the way the controller wrote it. Here is one ticket-granting ticket request from 11 March, read from the Module 0 evidence folder, where it is exported in the XML Windows writes:

PS C:\> $e = Get-WinEvent -ComputerName NE-DC01 -FilterXPath "*[System[EventRecordID=$id]]" -LogName Security
>> $x = [xml]$e.ToXml()
>> $d = [ordered]@{ TimeCreated = $x.Event.System.TimeCreated.SystemTime; Computer = $x.Event.System.Computer; EventID = $x.Event.System.EventID }
>> $x.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.InnerText }
>> [pscustomobject]$d | Format-List
TimeCreated          : 2026-03-11T07:04:07.000Z
Computer             : NE-DC01.corp.ne.com
EventID              : 4768
TargetUserSid        : S-1-5-21-3623811015-3361044348-30300820-1546
TargetUserName       : w.rathbone
TargetDomainName     : NE
ServiceName          : krbtgt
ServiceSid           : S-1-5-21-3623811015-3361044348-30300820-502
TicketOptions        : 0x40810010
TicketEncryptionType : 0x12
PreAuthType          : 2
IpAddress            : ::ffff:10.0.3.57
IpPort               : 51677
Status               : 0x0

The command is shown as you would run it against NE-DC01's live log, fetching one record by its number; the output comes from l3-tgt-event.xml, the same event exported, which you can download from the free evidence folder and parse yourself. The fields are the ones Microsoft's reference for event 4768 documents, and the ones that carry meaning, annotated:

One row of the corpus, field by field Security event 4768 on NE-DC01 TimeCreated 2026-03-11T07:04:07Z UTC, as every timestamp here Computer NE-DC01.corp.ne.com where it was recorded EventID 4768 TargetUserName w.rathbone who the ticket is for ServiceName krbtgt TicketEncryptionType 0x12 AES256, the expected key PreAuthType 2 IpAddress ::ffff:10.0.3.57 the client, in IPv6 form Status 0x0 granted Every row in SecurityEvent is a Windows event like this one, with its EventData flattened into columns.

Three fields do most of the work in this course. TargetUserName says who; IpAddress says from where, written ::ffff: before the IPv4 address when the request arrived over IPv4; and Status says what the controller decided, 0x0 for granted and a reason code otherwise.

TicketEncryptionType 0x12 is AES256, which Microsoft's reference lists, with 0x11, as the values to expect from current Windows; anything else is worth a look, and Module 9 explains why. PreAuthType 2 is the ordinary case: the account proved who it was with a timestamp encrypted with its own key before the controller issued anything, which is what pre-authentication means.

Four of these fields are also columns of SecurityEvent: TargetUserName, IpAddress, ServiceName and Status, so a query can test them directly. The rest, such as the encryption type and the ticket options, stay inside the EventData column as the XML Windows wrote, and a query extracts them when it needs one. Here is the ticket options field pulled out of every ticket-granting ticket request in the month:

SecurityEvent
| where EventID == 4768
| extend TicketOptions = extract(@'Name="TicketOptions">([^<]+)', 1, EventData)
| summarize Requests = count() by TicketOptions
| sort by Requests desc

Two rows: 4,104 requests carrying 0x40810010, forwardable, renewable and canonicalize, the flags a Windows client normally sets, and 17 with no value at all, which are the refused requests, because a refusal issues no ticket and so has no options to record.

A rule of thumb follows that holds across the course: when a column is empty, look at EventData before deciding the field is missing. In Splunk the same fields carry Splunk's names, TargetUserName is Account_Name and Status is Result_Code, and every field is already extracted. The query blocks in each lesson use whichever names each language expects, and the course's detection library lists both.

5

Sysmon and the Other Logs

investigate: what the Event table holds

The Event table holds everything collected that isn't the Security log, and in this corpus that means four logs. Sysmon is 14,418 of the 14,641 rows; the System log 133, Directory Service 87 and Application 3. Sysmon matters because it sees what the Security log doesn't: which program ran, with which command line, what it connected to and what it opened. Count what it recorded:

Five event types, with process creation the largest by far at 8,203. Microsoft's Sysmon documentation describes event 1 as extended information about a newly created process, with the full command line, which is why it is the event most lessons read when they need to know what an attacker ran.

Network connections, event 3, and DNS queries, event 22, say what a process reached; process access, event 10, records one process opening another, which is how credential theft from memory shows on a host.

The network row is itself a decision: Microsoft's documentation notes that Sysmon's network connection event is disabled by default, so 2,946 of them in the month means Northgate's configuration turns it on, and a domain whose configuration doesn't would have no row 3 at all.

The other three logs are small and specific. The System log, from the two writable controllers and a few member servers, records services starting, stopping and being installed; Directory Service records the controllers' own warnings, such as clients binding to LDAP without signing; Application holds three rows from ESENT, the database engine under the directory. Each matters to one or two lessons, which read it when they get there.

What matters now is knowing they are in Event, filtered by the EventLog column, rather than in a table of their own.

6

What Defender for Identity Adds

investigate: the Identity tables

Defender for Identity runs a sensor on each domain controller and writes its own tables, separate from the Windows logs, and they answer some questions the logs answer badly. The three tables together, by the kind of activity each row records:

union IdentityLogonEvents, IdentityDirectoryEvents, IdentityQueryEvents
| summarize Rows = count(), First = min(Timestamp), Last = max(Timestamp) by ActionType
| sort by Rows desc
index=defender sourcetype="ms:defender:eventhub"
| stats count AS Rows, min(Timestamp) AS First, max(Timestamp) AS Last BY ActionType
| sort - Rows

Four kinds of row. 1,924 logons the controllers validated; 178 directory replication requests; 116 LDAP queries and 15 SAMR queries against the directory.

Microsoft describes IdentityQueryEvents as queries performed against Active Directory objects such as users, groups, devices and domains, and that is the table's value: a Windows controller writes no event for an ordinary LDAP search, so the sensor's view is the only record of who looked at the directory.

Several modules lean on it for exactly that reason. The same holds for replication: the controllers replicate with each other constantly and the Security log records it only for objects with an audit entry, while the sensor records each replication request with the account and the machine that made it.

A replication request from a machine that isn't a controller is the shape of one of the attacks the course teaches, and it is in this table that the course reads it.

The course's rule for these tables follows from where they come from. Every detection in the course is written in KQL and SPL against the same evidence, and then in PowerShell and Sigma, which read the Windows logs on a host.

A detection built on a source that exists only in Defender for Identity has no Windows event to read, so it is written in KQL and SPL only, and the lesson says so. You'll meet that note in the modules on enumeration and replication.

7

The Evidence Files

act: reading directory state

Events say what happened; many questions about Active Directory are about how it is set up, and a log can't answer those. Who holds delegation, what a certificate template allows, which accounts can replicate secrets: these are state, read from the directory with PowerShell or a console, and the course ships them as evidence files:

An evidence file

D8 of the course specification, and the files every module ships

What it holds

directory state at a moment: an export of users, groups, ACLs, templates, delegation, trusts or settings, or a day of events from a log the SIEM does not hold

Its shape

what the command would return, in Microsoft's documented format: 568 JSON exports and 234 Windows event XML files among the 854 in Modules 1 to 24

Where it lives

/data/ads-evidence/ads00/ to ads24/, one folder per module, named for the lesson that reads it: l3- is lesson 3

Who can read it

ads00 is free with this module; every other folder needs a subscription, like the lessons that read it

How a lesson shows it

the command as you would type it on a domain host, and its output produced by running PowerShell on the file

A lesson shows an evidence file the way you would meet it at work. The command is written as you would type it on a host in the domain, and the output is what PowerShell produced when it ran on the file, with nothing typed by hand. Module 0's own example is the domain object, as Get-ADDomain returns it:

PS C:\> Get-ADDomain | Format-List DNSRoot, NetBIOSName, DomainMode, PDCEmulator, InfrastructureMaster, ReadOnlyReplicaDirectoryServers
DNSRoot                         : corp.ne.com
NetBIOSName                     : NE
DomainMode                      : Windows2016Domain
PDCEmulator                     : NE-DC01.corp.ne.com
InfrastructureMaster            : NE-DC01.corp.ne.com
ReadOnlyReplicaDirectoryServers : {NE-RODC01.corp.ne.com}

The domain is corp.ne.com, NetBIOS name NE, at the Windows Server 2016 functional level, with NE-DC01 holding the PDC emulator and infrastructure master roles and one read-only controller, NE-RODC01.

Microsoft's reference for Get-ADDomain returns these properties under these names, and the file is lesson 1's export of the same object, cut to the properties shown, so the two agree. Lesson 0.2 walks the rest of Northgate's domain, and Module 1 reads the full export.

Producing a file like this in your own domain takes one line, and it is worth doing before any change you make. ConvertTo-Json keeps the property names and the nesting, so the file can be read back and compared with the next export:

Get-ADDomain | ConvertTo-Json -Depth 4 | Set-Content ".\domain-$(Get-Date -Format yyyyMMdd).json" -Encoding utf8

Dating the file name is the habit that matters. Two exports of the same object from different days, compared field by field, are how a later lesson proves a change happened and stayed made, and an undated export proves nothing about when it was true.

Every module has its folder: 854 files across Modules 1 to 24, most of them JSON exports and Windows event XML. The ones in ads00 are free with this module, so you can open them now; the rest need a subscription, as the lessons that read them do.

If you build the optional lab domain that ads92 sets up, you can run the same commands against it and compare your output with the file, which is the best way to learn what the output means.

8

Modeled Dates, and How Lessons Label Them

verify: where any fact comes from

Every number in a lesson has one of three origins, and you can tell which from the page without reading the generator that built it. The test is two questions about the fact in front of you:

Where a fact in a lesson comes from Is it dated after 11:53 on 15 March? the last row in the corpus Modeled an evidence file, and the lesson says so Is it something that happened? a sign-in, a change, a request State: an evidence file what an export showed at that moment An event in the corpus query it in KQL or SPL yes no no yes Two questions place any number in a lesson, and the lesson names its source either way.

Anything after 11:53 on 15 March is modeled, because the corpus ends there. A lesson that tests a change on 16 April shows the controller's log for that day as an evidence file and says the date is modeled where it first appears; a lesson that reads the month queries the corpus.

State is always a file, dated to when it was exported. Events in the month are always queries you can run.

In a lesson that reads like this, from Module 13: everything after 15 March in this lesson is modeled, because the corpus ends there, and the exports, the change record and both runs are the course's evidence files rather than events in the log. The sentence appears once, where the dates begin, and the page conventions make each origin visible after that:

How to tell where a lesson's fact comes from

The conventions every lesson follows

A query block with a Run button

an event in the corpus month; run it and the rows are the ones the prose quotes

A command shown on a host, with output

an evidence file; the output came from PowerShell run on that file

A date after 15 March

modeled; the lesson says "modeled" where the date first appears

A date on or before 15 March

the corpus, or a state export taken in that month

A count in a figure

computed from the corpus or the files when the lesson was built, never typed

The labels exist because a course that mixes real queries with prepared files owes its reader the difference. A query's rows are something you can reproduce; a file's contents were prepared for the lesson, in the shape Microsoft documents and consistent with every other file and with the corpus, but prepared.

When a lesson says a detection returns two rows in the month, run it. When it shows a change made in May, read it as the course's own record of that work, kept consistent from module to module, because a change in Module 13 stays made in Module 21.

Lesson 0.4 puts all of this to use: one question answered from the corpus, in KQL and SPL, and checked a third way against an exported day of the controllers' own logs.

Practice

Do this Map the evidence your own domain keeps
  1. List the logs. For each domain controller and certificate authority, write down whether its Security log reaches your SIEM, and since when.
  2. Count the tables. Run the first query of section 1 against your own data, and compare the machines it returns with the controllers your directory lists.
  3. Find the gaps. For each table in section 2, say whether you have its equivalent; where you don't, note which module of this course depends on it.
  4. Export one piece of state. Run Get-ADDomain and save the output as JSON, dated; that file is your first evidence file.
  5. Label the dates. Write on it what it is: an event, a state export, or a note of something planned.
What you should end up with: a one-page list of which logs your domain sends where, the tables you can query, the ones you can't, and a dated export of your domain object.

Next, lesson 0.4: your first investigation, one question answered end to end on the corpus.