In this section

0.3 The Recording This Course Reads

Module 0

Introduction

The attacker leads, and everything the attacker did is written down twice: once by the operator, and once by the range's defense. This course is built on that double record. Every test the engagement ran, every record the hosts and services kept, and every alert, incident and note the defense made are in one recorded dataset, the recording, and every lesson queries it in the console.

This lesson walks through the recording's tables, its days and its two time columns, so that when a lesson says a test left no record, you can trust that the query was looking in the right place.

The recording is also the course's shared vocabulary. When a lesson refers to PT-0103, PU-R02 or Run 6, it means a specific row, rule or day you can open in the console, and two students discussing the course can point at the same records.

That is harder in a course where every student's lab produces different data, and it is why the course uses both: the recording for the answers, your lab for the practice.

The recording runs from Thursday 10 September, a few quiet days before the first test, to Tuesday 29 September at 18:00 UTC. It is generated from a script with fixed seeds, so every student who runs a query gets the same answer, and every answer a lesson prints is checked against it when the lesson is built.

Reading a recording is different from reading a live workspace in one important way: nothing new arrives. That is what makes the course's results stable and comparable, and it is also why your own lab, which you build in Lessons 0.7 to 0.11, matters. The recording shows you what a known engagement looked like; your lab shows you what your own environment does with the same procedures.

1

Plan the round

How every query against the recording is written

A few rules apply to every query in the course, and they are the reason a query's answer means what it says:

Reading rules for the recording
TimeEvery time is UTC; the hosts' local times are not used
Defender tablesFilter on Timestamp
Every other tableFilter on TimeGenerated, or the table's own time column
BoundsEvery query bounded to the window it asks about
Decided byWhether an empty result could only mean the record is absent
Either wayThe query that returned it is kept with the answer

The decision line is the one this lesson is about. A query can return nothing for two reasons: the record does not exist, or the query was written so that it could not find it.

The second is surprisingly easy: the wrong time column, a window in the wrong hour, a table that had not started recording yet. Each rule above closes one of those, so that an empty result can be read as an absence.

Bounding every query is the rule that most often seems fussy and most often matters. The recording holds three weeks; the same procedure was run on several of those days, deliberately, as originals, retests and variants; and an unbounded query mixes them. Section 10 shows the difference on a single program, and you will meet it again whenever a lesson compares a test with its retest.

Keeping the query with the answer is the course's reporting habit. A count with no query beside it cannot be checked, and a count that cannot be checked is not evidence. Every result in every lesson is printed under the query that produced it for exactly that reason.

2

The attack: a Caldera operation, and the table it writes

PT-0404, the Enumerator adversary under the atomic planner

The recording starts where the attacks start. On Friday 18 September the operator ran a Caldera operation against the workstation: the Enumerator adversary from MITRE's Stockpile plugin, five abilities that list processes and check the user's privileges, under the atomic planner, one ability at a time. Caldera keeps its own record of every link it sends, and the range forwards that record to the workspace as PurpleCalderaEvents_CL:

PurpleCalderaEvents_CL
| where Operation == "Friday Enumerator (atomic)"
| project DelegatedTimestamp, AbilityName, AbilityId, Pid, Status
| order by DelegatedTimestamp asc

Five links, each an ability with its ID, the process the agent started for it, and its status:

Delegated  Ability                             Id        Pid   Status
---------  ----------------------------------  --------  ----  ------
11:00:30   WMIC Process Enumeration            94f21386  7300  0
11:01:35   tasklist Process Module Enumera...  8adf02e8  7301  0
11:02:40   PowerShell Process Enumeration      4d9b079c  7302  0
11:03:45   UAC Status                          7c42a30c  7303  0
11:04:50   SysInternals PSTool Process Dis...  cc191baa  7304  1

Status 0 is success. The fifth ability, the Sysinternals PSTools download, returned 1, because the range's allowlist refused the download, and that refusal is a result about the range's edge. Each link's command is in the Command field, exactly as the agent ran it, so a defender can match every process on the workstation to the ability that started it by its PID.

3

Run it: an Atomic test, with the execution log on

PT-0403, and the row the tool wrote

An Atomic test leaves the operator's own record too, when the operator asks for one. Earlier that morning the operator ran T1069.002 test 8, AdFind's group query, by GUID with the execution log on:

PS C:\> $g = '48ddc687-82af-40b7-8472-ff1e742e8274'
PS C:\> $log = 'C:\PurpleArtifacts\art.csv'
PS C:\> Invoke-AtomicTest T1069.002 -TestGuids $g -ExecutionLogPath $log

Invoke-AtomicTest's default logger appends one CSV row per run, with these columns, and the range forwards each row to the workspace as PurpleExecutionLog_CL:

"Execution Time (UTC)", "Execution Time (Local)", "Technique",
"Test Number", "Test Name", "Hostname", "IP Address",
"Username", "GUID", "ProcessId", "ExitCode"

The row for PT-0403, as the workspace holds it:

PurpleExecutionLog_CL
| where TimeGenerated between (datetime(2026-09-18T10:20:00Z)
    .. datetime(2026-09-18T10:21:00Z))
| project TimeGenerated, Technique, TestName, Hostname, ExitCode

One run, with the tool's own time, host, user, GUID and exit code:

Time      Technique  Test                            Host        Exit
--------  ---------  ------------------------------  ----------  ----
10:20:04  T1069.002  Adfind - Query Active Direc...  NE-PT-WS01  0

Exit code 0: AdFind ran and returned. The Caldera links and the execution log are two of six tables the purple team writes itself. Their row counts across the recording:

From the operator's seat the recording begins with six tables the purple team writes itself. Their row counts:

union
    (PurpleTestLog_CL | summarize Rows = count()
    | extend Table = "PurpleTestLog_CL"),
    (PurpleExecutionLog_CL | summarize Rows = count()
    | extend Table = "PurpleExecutionLog_CL"),
    (PurpleCalderaEvents_CL | summarize Rows = count()
    | extend Table = "PurpleCalderaEvents_CL"),
    (PurpleReadiness_CL | summarize Rows = count()
    | extend Table = "PurpleReadiness_CL"),
    (PurpleCollabLog_CL | summarize Rows = count()
    | extend Table = "PurpleCollabLog_CL"),
    (PurpleReplay_CL | summarize Rows = count()
    | extend Table = "PurpleReplay_CL")
| project Table, Rows

Six operator tables, from the test log down to a single replay:

Table                   Rows
----------------------  ----
PurpleTestLog_CL        78
PurpleExecutionLog_CL   41
PurpleCalderaEvents_CL  24
PurpleReadiness_CL      15
PurpleCollabLog_CL      18
PurpleReplay_CL         1

The test log, PurpleTestLog_CL, has one row per test: its ID, run, technique, Atomic GUID where there is one, target, operator, command, status and cleanup. Every other table hangs off it.

The execution log, PurpleExecutionLog_CL, is written by Invoke-AtomicTest for each Atomic run, with the exit code the operator's status should agree with; it starts on 18 September, when the team turned it on. The Caldera table, PurpleCalderaEvents_CL, has one row per link of every Caldera operation, with the times the link was delegated, collected and finished.

The other three record the exercise around the tests. PurpleReadiness_CL holds the operator's checks of the range itself, each with what was expected and what was observed. PurpleCollabLog_CL holds the exercise chat, the bridge notes and the SOC ticket for the collaborative day. PurpleReplay_CL holds one record replayed into the workspace to test a rule, which section 4 explains.

The test log's status column deserves a warning on day one. It is set by the operator, and it says whether the procedure ran as planned. It does not say whether the procedure succeeded; that is the exit code in the execution log, where there is one. A blocked test can be logged Completed with a non-zero exit, and the course reads both columns before it classifies anything.

The operator's tables are short because they describe intent and outcome, not activity. Seventy-eight test rows are matched by hundreds of records in the defense's tables, and the defender's first move with any test is to start from its row and find those records.

4

The variant: a record replayed

Testing a rule without running the test again

The operator has one more way to exercise a rule, and the recording holds one example. The replay table:

PurpleReplay_CL
| project TimeGenerated, ReplayOf, SourceTable, OriginalTimestamp, ActionType

One record, a copy of PT-0103's task creation, written back into the workspace on 18 September:

Time              Replays  Table         Original          Action
----------------  -------  ------------  ----------------  --------------------
2026-09-18 13:40  PT-0103  DeviceEvents  2026-09-15 09:40  ScheduledTaskCreated

A replay tests a rule against a record that a real test produced, without running the procedure again. It is quick and safe, and it proves one thing only: that the rule matches that record. It does not prove the procedure would produce the same record today, or that the alert would reach anyone. The operator's note on the method:

Operator's note: replay against rerun
ReplayProves the rule reads a known record
RerunProves the procedure still produces it, and the defense still sees it
Use a replayWhile writing or repairing a rule
Use a rerunTo close a gap
NeverReport a replay as a retest

The course uses reruns to close gaps throughout, and mentions the replay here so that you recognize it when Module 4 uses it once.

5

Detection follows: the defense's tables

What the hosts, services and analysts recorded

The defense's side of the recording is larger. The row counts of its main tables:

union
    (DeviceProcessEvents | summarize Rows = count()
    | extend Table = "DeviceProcessEvents"),
    (DeviceRegistryEvents | summarize Rows = count()
    | extend Table = "DeviceRegistryEvents"),
    (DeviceEvents | summarize Rows = count()
    | extend Table = "DeviceEvents"),
    (DeviceNetworkEvents | summarize Rows = count()
    | extend Table = "DeviceNetworkEvents"),
    (SecurityEvent | summarize Rows = count()
    | extend Table = "SecurityEvent"),
    (SigninLogs | summarize Rows = count()
    | extend Table = "SigninLogs"),
    (SecurityAlert | summarize Rows = count()
    | extend Table = "SecurityAlert"),
    (SecurityIncident | summarize Rows = count()
    | extend Table = "SecurityIncident")
| project Table, Rows
| order by Rows desc

Eight of the defense's tables, from the largest down:

Table                 Rows
--------------------  ----
DeviceProcessEvents   257
DeviceNetworkEvents   243
SecurityEvent         102
SigninLogs            96
DeviceEvents          66
SecurityAlert         44
DeviceRegistryEvents  41
SecurityIncident      23

The defense's tables are written by the hosts and services, not by the purple team, and nothing in them knows a test was running. A process record from a test looks exactly like a process record from a person, which is the point: the defense has to recognize the activity from what it is, not from a label. The purple team's job is to tie the two records together afterward.

The Defender tables hold the workstation's and the server's activity: processes, registry changes, network connections and device events such as a scheduled task created or a tampering attempt blocked. SecurityEvent holds the Windows Security log from the server and the domain controllers.

SigninLogs holds the tenant's sign-ins from 22 September. SecurityAlert and SecurityIncident hold what the defense did: the alerts its rules raised and the incidents analysts opened, with their verdicts and comments.

Beyond these eight, the recording has Syslog from the Linux host, the PowerShell log in Event, the identity sensor's tables, the tenant's audit and Exchange records, DNS and the network sensor's connections. Lesson 0.2 gave the date each began.

The ordinary activity in every table is there too, the nightly inventory, the browser, the helpdesk's queries, because a rule has to be shown to leave that alone as well as to catch the tests.

6

The runs, day by day

Nine runs over seven days

Tests are grouped into runs, each a planned set of tests on one day. The test log, by day and run:

PurpleTestLog_CL
| summarize Tests = count() by Day = format_datetime(TimeGenerated, "yyyy-MM-dd"), Run
| order by Day asc, Run asc

Nine runs on seven days, one day with two runs:

Day         Run  Tests
----------  ---  -----
2026-09-15  1    14
2026-09-17  2    4
2026-09-17  3    13
2026-09-18  4    11
2026-09-21  5    12
2026-09-22  6    8
2026-09-24  8    9
2026-09-28  10   2
2026-09-29  11   5

The first run, on Tuesday 15 September, is the exercise week's opening: fourteen tests across the workstation, the server and the Linux host. Thursday 17 September has two runs, the morning retests of four repairs and the afternoon's attacks on those repairs.

Friday is the tooling day, Monday 21 September the endpoint validation day, Tuesday 22 September the identity, network and cloud day, Thursday 24 September the retest day, Monday 28 September the collaborative exercise, and Tuesday 29 September the unattended regression run.

A run is also the unit of planning. Each was written up before it started, with its tests, expected outcomes and any white cards, and each ended with a review. The runs table is the engagement's calendar, and the course's modules follow it closely enough that you can tell from a lesson's dates which run it is reading.

Each run matches a module of the course, roughly in order, and each module's lessons read mostly from its own run. That is why a lesson's queries are bounded to a day: the same procedure appears on several days, deliberately, and a query that is not bounded will mix an original with its retest.

7

Two time columns

Timestamp in Defender, TimeGenerated elsewhere

The recording keeps the time column each table has in production, and the two kinds differ. One process record's two columns:

DeviceProcessEvents
| take 1
| project Timestamp, TimeGenerated

One record, with one of its two time columns filled and the other empty:

Timestamp            TimeGenerated
-------------------  -------------
2026-09-29 10:40:12  None

Defender's advanced hunting tables carry their event time in Timestamp; the Sentinel tables and the operator's tables carry TimeGenerated.

A query that filters a Defender table on TimeGenerated returns nothing at all, and nothing at all looks exactly like a test that left no record. It is the single most common mistake in reading the recording, and every query in the course that touches a Defender table filters on Timestamp for that reason.

The opposite mistake is rarer and just as quiet: filtering an operator's table on Timestamp, which it does not have. The console reports an error for an unknown column in most cases, which is kinder than an empty result, but a query that unions several tables can hide the error in one branch. The course writes each branch's time column explicitly for that reason.

Every time in the recording is UTC. The workstation's execution log also carries a local time, an hour ahead, because the tool writes both; the course always uses the UTC column, and the lessons say so wherever the two could be confused.

8

Days left out

Two days kept for the staged assessments

Two days are missing from the recording on purpose. Wednesday 23 September and Friday 25 September are the recorded days of the course's first two staged assessments, and the capstone's assessment has two days of its own after the recording ends. None of them is in the dataset the lessons and the console read.

The days held back, and what each is for:

Recorded days held back from the lessons
Wednesday 23 SeptemberStaged Assessment 1, after Module 5: an endpoint control's verdict
Friday 25 SeptemberStaged Assessment 2, after Module 7: a failed detection repaired
30 September and 1 OctoberStaged Assessment 3, the capstone: the whole exercise
Where they liveIn each assessment's own dataset, loaded by the console
In the lessonsNever quoted, never queryable

The reason is simple: a staged assessment asks you to find things in a day you have not seen, and if that day were in the console, any lesson's query could stumble onto its answers.

Keeping the days out means every assessment is a genuine first look. The runs table in section 3 shows the gap, with Runs 7 and 9 absent and no tests on the 23rd or the 25th.

What the recording holds, and what it leaves out10 to 14 Sepquiet days, readiness15 to 22 SepRuns 1 to 623 Sepassessment 1, left out24 SepRun 8, retests25 Sepassessment 2, left out28 to 29 SepRuns 10 and 11
Green days are in the recording, red days are held back for the staged assessments.

Holding days back has a cost the course accepts. A lesson cannot quote what happened on the 23rd or the 25th, so the story of the engagement has two gaps in it, and a few later lessons mention work done on those days without showing its records, because showing them would answer an assessment's questions.

Each lesson that crosses a gap says so in a sentence, so the gaps are never mistaken for missing data, and the assessment that holds each day is named where it matters.

The staged assessments are taken in the same console, with their own datasets loaded in place of the course's. When you open one, the console says which dataset it is reading.

9

The same events through Splunk

One recording, two query languages

The course teaches detections in KQL, SPL and Sigma, and the recording exists in a Splunk form as well, with the same events. Each source keeps the shape a Splunk deployment would give it:

The recording in Splunk
Defender tablessourcetype ms:defender:eventhub/<table>, index defender
Windows Security logXmlWinEventLog:Security, index wineventlog
Linuxsyslog, index linux
Entraazure:monitor:aad, index azure
Exchange audito365:management:activity
DNS and ZeekMSAD:NT6:DNS and bro:conn:json

The Splunk form is written by the same generator from the same rows, so a record that is in one form is in the other, with the same time and the same values. Differences between a KQL rule and its SPL translation are therefore differences in the rules, never in the data, and the course's rule checker uses that to catch a translation that drifts.

Every rule the course writes is run in both languages over the same events, and the two forms must return the same results before a lesson shows them. Lessons that teach SPL say which sourcetype they read; the rest of the course uses KQL by default, because that is the language of the platform the range runs on.

Sigma is the third form. A Sigma rule is a portable description of a detection, converted to a query for whichever platform runs it; the course gives each rule's Sigma source beside its KQL and SPL, and checks that it matches the same events on the recording where the range collects the log it reads.

10

The console

Where every query in the course runs

Every query on every page runs in the platform's console against the recording, with the Purple Range selected. Run any query a lesson shows and you get the result the lesson prints; change it and you get the answer to your own question. The console also holds the cases at the end of each lesson, where you write the queries yourself and the console checks your answers.

Working in the console follows the same few steps in every lesson:

Working in the console
SelectThe Purple Range, the course's recording
RunThe lesson's query as printed, and check it returns what the page shows
ChangeOne thing at a time: the window, a condition, a column
PinA result row with the star, when a case asks for evidence
AnswerIn the case, from what your own query returned

Changing one thing at a time is the habit worth building first. A query changed in three places at once that returns something unexpected cannot tell you which change mattered, and the console makes it cheap to change one thing and run again.

The console is the reason the recording is small enough to read. Two weeks of a range with three targets and a tenant is a few thousand records, which is enough to hold every kind of result the course teaches and few enough that a query returns instantly.

Your own lab will produce a different recording of your own tests, and Lessons 0.7 to 0.11 show how to read it with the same habits.

11

Validate

A query without a window, and with one

The obvious way to find PT-0103's process in the recording is to search for its program:

First tryevery schtasks.exe process in the recording
DeviceProcessEvents
| where FileName =~ "schtasks.exe"
| count
4 records. More than one: the same program ran on other days.
An unbounded query mixes the test with every other run of the same program, including the retests and variants of later days. Bounding it to the test's window, from its row, finds the one record that belongs to it.
Refinedschtasks.exe in PT-0103's five minutes
DeviceProcessEvents
| where Timestamp between (datetime(2026-09-15T09:40:00Z)
    .. datetime(2026-09-15T09:45:00Z))
| where FileName =~ "schtasks.exe"
| count
1 record. One process, the test's own.

The first try's extra records are not noise to be thrown away; they are the same program on other days, several of them later tests of the same technique.

A defender who wanted to compare PT-0103 with its retest would want exactly those records, each in its own window. The point is not that unbounded queries are wrong but that each answer should come from the window its question is about.

The refined query is the shape of almost every query in the course: a table, a window from the test's row, and a condition. On the chain both seats use, the recording reads:

Outcome chain

The recording

Executed

78 tests in nine runs

Recorded

Every source, with its start

Alerted

44 alerts

Handled

23 incidents with verdicts

Owner

Generated with fixed seeds

Plan

Assessment days held back

What the recording gives the course

The same answer for every student.

  • Six tables of intent and outcome.
  • The hosts', services' and analysts' records.
  • One console, every query reproducible.

A recording every lesson queries, small enough to read and complete enough to trust a silence.

The recording has limits of its own, and the course states them rather than hiding them. It is a reconstruction of one engagement on one small range, generated so that every result can be reproduced; its ordinary activity is a few predictable routines, not a busy office; and its rules are the range's, not a vendor's.

Those limits are why the course pairs it with your own lab, where the activity is your own and the surprises are real.

Every later lesson starts from this chain. When a lesson reports that a test was silent, the chain is how you know the silence was looked for in the right table, over the right window, with the right time column.

12

Adapt: four questions, and the tables that answer them

Where to look first

Every question a purple team asks of a recording is one of four, and each has its own tables:

Four questions, and where to look

What did the operator run?

The tables that answer itPurpleTestLog_CL, PurpleExecutionLog_CL, PurpleCalderaEvents_CL
VerdictOperator's tables: YesDefense's tables: No

Was the range ready?

The tables that answer itPurpleReadiness_CL
VerdictOperator's tables: YesDefense's tables: No

What did the hosts and services record?

The tables that answer itThe Defender tables, SecurityEvent, Syslog, Event, the Entra and network tables
VerdictOperator's tables: NoDefense's tables: Yes

What did the defense do?

The tables that answer itSecurityAlert, SecurityIncident, PurpleCollabLog_CL
VerdictOperator's tables: NoDefense's tables: Yes

Each question also has a typical mistake. Asking what the operator ran from the defense's tables credits the defense with tests it never saw; asking whether the range was ready from memory skips the checks that predicted half the results; asking what was recorded from the alert queue misses every silent test; and asking what the defense did from the rules alone misses every alert nobody opened.

The first two questions are answered by the purple team's own records, the last two by the defense's. A lesson always asks them in that order: what ran, whether the range was ready, what was recorded, and what the defense did. Starting from the third or fourth question, as a dashboard does, leaves the first two unasked, and they are where the most surprising results begin.

Try

Learn the recording's tables

The case below reads the operator's tables and the runs, then the defense's tables and their time column. Work in the platform's Advanced hunting console with the Purple Range selected.

All four answers come from short queries over whole tables, which is the right way to learn what a recording holds before asking it anything specific. Most of the course's queries are narrower, bounded to one test's window, and you will write those from Lesson 0.4 onward.

The fourth answer is the one that will save you the most time: when a Defender query returns nothing, check the time column before concluding the record is absent.

Do

Practice

Find a record in the right table
your lab

The practice is in the console, against the recording; your own lab is built in Lessons 0.7 to 0.11.

In your labFind a record in the right table
1

Operator: a test

Pick any test in Run 5 from the test log and note its time and host.

2

Defender: the wrong column

Query DeviceProcessEvents for its window on TimeGenerated, and note what comes back.

3

Defender: the right column

Run the same query on Timestamp, and note what comes back.

4

Both: the lesson

Write one sentence on why the first result was empty.

What you should end up with: A test's records found with the right time column, and the empty result explained.

Resources

Publishers revise these pages; where a page and this lesson disagree, the page is newer.