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.4 Your First Test
Introduction
The attacker leads, and in this lesson you follow.
PT-0104 is the engagement's fourth test: at 10:00 on Tuesday 15 September the operator added a value to the workstation's Run key, the place Windows reads at every logon to start programs for the user. It is one of the most common ways an intruder makes a foothold survive a reboot, and Atomic Red Team's T1547.001 test 1 does it with a single reg.exe command.
You will read the test in the course's six steps, from the operator's row to the analyst's verdict, and classify it yourself.
You do not need your own lab for this lesson, or for any lesson in Module 0. Everything runs in the console against the recording, and your lab comes in Lessons 0.7 to 0.11, once you know what a test's records should look like.
Persistence is a good first technique for another reason: its evidence is a change that should not be there. Discovery leaves queries that look like ordinary administration, and credential access often leaves very little; persistence leaves a key, a task or a service that the defender can point at. Reading one clean case first makes the harder ones in later modules easier to recognize.
The six steps are the same in every lesson of the course, and the case at the end asks you to work them in the console. Nothing in the lesson depends on knowing the registry well; each step says what to look for and why. What matters is the order, because each step's answer bounds the next step's query.
PT-0104 is a good first test because every step has an answer and the last one has a surprise. The registry recorded everything, the rule fired, and the incident was closed with a verdict that was right about the cause and wrong about the reason, which is the kind of result a purple team exists to notice.
Plan the round
The six steps, in orderEvery test in the course is read in six steps, each answered from a different record:
1 The row
2 The records
3 The cleanup
4 The rule
5 The alert
6 The verdict
The order is the point. The row bounds every later query, so it comes first. The records come before the rule, because a rule cannot be judged until you know whether the record it should read exists.
The cleanup comes before the alert, because a test that did not clean up is still on the host, and its traces could be mistaken for a second intrusion. The verdict comes last, because it depends on everything before it.
The decision line for the reading, written the same way as every plan in the course:
The decision line is the test of a good reading. If another analyst reran your six queries and reached a different classification, one of you misread a record, and the records will say which. A reading that cannot be checked this way is an opinion about the test, and the course does not report opinions as results.
Reading in this order also makes the work faster than it sounds. Each step is one short query, the row's window keeps every query small, and the six together take a few minutes once the habit is formed.
Most of the time in a purple team exercise goes into the tests that turn out to be surprising, and the six steps are how you find out quickly which ones those are.
The attack: step 1, Atomic T1547.001 test 1
The Run key, set by Invoke-AtomicTestYour first test is persistence through the Run key, the oldest way to make Windows start a program at every logon. Atomic Red Team's T1547.001 test 1, Reg Key Run, GUID e55be3fd-3521-4610-9d1a-e210e42dcf05, writes a value there with reg.exe:
REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" ^
/V "Atomic Red Team" /t REG_SZ /F /D "#{command_to_execute}"
The input argument command_to_execute defaults to C:\Path\AtomicRedTeam.exe, a program that does not exist, so the persistence is real but nothing would start. The cleanup deletes the value:
REG DELETE "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" ^
/V "Atomic Red Team" /f >nul 2>&1
The operator's sequence, the same for every Atomic test in the course: read the test, check its prerequisites, run it, clean up.
PS C:\> $g = 'e55be3fd-3521-4610-9d1a-e210e42dcf05'
PS C:\> Invoke-AtomicTest T1547.001 -TestGuids $g -ShowDetailsBrief
PS C:\> Invoke-AtomicTest T1547.001 -TestGuids $g -CheckPrereqs
PS C:\> $log = 'C:\PurpleArtifacts\art.csv'
PS C:\> Invoke-AtomicTest T1547.001 -TestGuids $g -ExecutionLogPath $log
PS C:\> Invoke-AtomicTest T1547.001 -TestGuids $g -Cleanup
This test has no prerequisites and needs no elevation, because it writes under the current user's own key. Run today, the execution log line records the tool's own account of the run; on Tuesday 15 September, when the operator ran it as PT-0104, the range's execution log was not yet turned on, so the test log row is the only operator record:
From the operator's seat the test is one row. PT-0104's:
PurpleTestLog_CL
| where TestId == "PT-0104"
| project TimeGenerated, Technique, AtomicTestName, TargetHost, Operator,
ExecutionStatus, CleanupTime
The operator's whole account of the test, in one row:
Time Technique Test Host Account Status Cleanup
-------- --------- ----------- ---------- ----------- --------- --------
10:00:08 T1547.001 Reg Key Run NE-PT-WS01 pt.operator Completed 10:02:08
The row is the only place the test is described in the operator's own words, and it is worth reading slowly the first time. Each field answers a question the defender would otherwise have to guess at: which host, which account, which minute, and whether anything should be left behind. Guessing any of them is how a defender ends up reading the wrong records.
Everything the defender needs to start is here. The time, 10:00:08, opens the window. The host, NE-PT-WS01, says which tables to read. The account, pt.operator, says whose records to look for. The cleanup time, 10:02:08, says when the test's change should disappear. The status says the procedure ran as planned.
The status says Completed, and for this test that is enough on its own. The execution log, which would carry the tool's exit code, did not exist yet on 15 September; it was turned on three days later. For the exercise week the defender confirms success from the records themselves, which is what the next step does.
The command itself is in the row as well: reg.exe adding a value named "Atomic Red Team" under the current user's Run key, pointing at a program path. Atomic Red Team publishes the command with the test, so the operator does not invent it and the defender can read it in advance. Knowing the command before reading the records is a large part of reading them quickly.
Run it: steps 2 and 3, the records and the cleanup
What the workstation recorded, and what the test removedThe defender starts with the processes in the test's first three minutes:
DeviceProcessEvents
| where Timestamp between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:03:00Z))
| project Timestamp, FileName, AccountName, InitiatingProcessFileName
Two processes, cmd.exe and reg.exe, both under the operator's account:
Time Process Account Parent
-------- ------- ----------- --------------
10:00:08 cmd.exe pt.operator powershell.exe
10:00:09 reg.exe pt.operator cmd.exe
Reading the processes first, before the registry, is a small discipline with a large payoff. The processes show that the procedure ran, under the right account, from the right parent, at the right time. If they were missing, there would be no point looking for the registry change, because the test would not have done anything; the defender would go back to the operator.
Invoke-AtomicTest ran the command through cmd.exe, which started reg.exe a second later. The account matches the row, so these are the test's. Then the registry. The window is slightly longer, five minutes rather than three, so that it covers the cleanup the row says happened two minutes after the start:
DeviceRegistryEvents
| where Timestamp between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:05:00Z))
| project Timestamp, ActionType, RegistryValueName, RegistryValueData
Two registry records, one value written and then removed:
Time Action Value Data
-------- -------------------- --------------- -------------------------
10:00:09 RegistryValueSet Atomic Red Team C:\Path\AtomicRedTeam.exe
10:02:08 RegistryValueDeleted Atomic Red Team (removed)
The registry records also carry the data the value pointed at, C:\Path\AtomicRedTeam.exe, a program that does not exist. Atomic Red Team's tests often point at harmless or absent files so that the persistence is real but nothing would run at the next logon. A defender should not conclude from a missing file that the persistence was harmless; an intruder's would point at something real.
The value was set at 10:00:09 and deleted at 10:02:08. Every step the procedure took left a record, so the test reached the second of the seven measures, telemetry, before any rule was involved.
The deletion at 10:02:08 is the cleanup the operator's row promised, at exactly the time the row gives. The defender checks it because an Atomic test's cleanup is part of the procedure: a test that did not clean up would leave a Run key value that starts a program at every logon, and a defender reading the host a week later would find what looks like an intruder's persistence.
The operator's note on the cleanup and what it does not cover:
The last line is the operator's next test, not this one's. Moving the user's Startup folder through the User Shell Folders key reaches the same result, a program started at logon, without writing a Run value at all, and Module 7 of the course tests it against PU-R03. For now the cleanup is complete, and the defender moves on to the rule.
Checking the cleanup is also how the defender separates the test from anything that happens after it. Once the value is gone, any later write to the same key is not PT-0104's, whatever it is called. The cleanup time draws the test's boundary on the host, as the start time draws it in the log.
The variant: the same Run key, written by hand
PT-0304, reg.exe with a path chosen against the ruleTwo days later the operator wrote a Run key again, by hand this time, after reading PU-R03 as it stood:
DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where RegistryKey endswith @"\CurrentVersion\Run"
| where RegistryValueData !contains @"\Program Files"
and RegistryValueData !contains @"C:\Windows\"
| where RegistryValueData !contains @"\AppData\Local\Microsoft\OneDrive\OneDrive.exe"
| project Timestamp, DeviceName, RegistryValueName, RegistryValueData,
InitiatingProcessFileName
The rule leaves out three kinds of path, and the third is OneDrive's, so OneDrive's own updater does not alert. The operator's variant, PT-0304, put the program under a folder whose path contains OneDrive's:
REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" ^
/V "OneDriveUpdate" /t REG_SZ /F ^
/D "C:\ProgramData\Updater\AppData\Local\Microsoft\OneDrive\OneDrive.exe"
What the workstation recorded, set and then removed by the operator two minutes later:
DeviceRegistryEvents
| where Timestamp between (datetime(2026-09-17T13:20:00Z)
.. datetime(2026-09-17T13:25:00Z))
| where RegistryKey endswith @"\CurrentVersion\Run"
| project Timestamp, ActionType, RegistryValueName, RegistryValueData
The value and its data, the same procedure under a different path:
Time Action Value Data
-------- -------------------- -------------- --------------------------------
13:20:09 RegistryValueSet OneDriveUpdate C:\ProgramData\Updater\AppDat...
13:22:08 RegistryValueDeleted OneDriveUpdate (removed)
No alert followed. The path contains OneDrive's executable path, so the rule's third exclusion matched, though the program sits in C:\ProgramData, nowhere near a real OneDrive install. The defense recorded the change in full and the rule chose not to read it, which is the kind of gap a purple team exists to find, and the one Module 7 repairs.
Detection follows: step 4, the rule
Whether PU-R03 should match the recordBefore reading the alert, the defender reads the rule, to know whether it should have fired. PU-R03 as it stood on the first day:
DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where RegistryKey endswith @"\CurrentVersion\Run"
| where RegistryValueData !contains @"\Program Files"
and RegistryValueData !contains @"C:\Windows\"
| where RegistryValueData !contains @"\AppData\Local\Microsoft\OneDrive\OneDrive.exe"
| project Timestamp, DeviceName, RegistryValueName, RegistryValueData,
InitiatingProcessFileName
The rule reads a value set under a key ending in CurrentVersion\Run, whose data points outside Program Files, outside the Windows folder, and is not OneDrive. PT-0104's value pointed at C:\Path\AtomicRedTeam.exe, which is none of those, so the rule should match. Running the rule over the test's window confirms it:
DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where RegistryKey endswith @"\CurrentVersion\Run"
| where RegistryValueData !contains @"\Program Files"
and RegistryValueData !contains @"C:\Windows\"
| where RegistryValueData !contains @"\AppData\Local\Microsoft\OneDrive\OneDrive.exe"
| project Timestamp, DeviceName, RegistryValueName, RegistryValueData,
InitiatingProcessFileName
| where Timestamp between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:05:00Z))
The rule, bounded to the test's five minutes, returns exactly one record:
Time Value Data Process
-------- --------------- ------------------------- -------
10:00:09 Atomic Red Team C:\Path\AtomicRedTeam.exe reg.exe
The rule's exclusions are worth reading too, because they say what its author decided was ordinary.
Values pointing into Program Files or the Windows folder are excluded because installers write them; OneDrive's own path is excluded because it writes one at every sign-in. Each exclusion is a place an intruder could hide, and the operator reads them the same way: a value pointing into Program Files, written by an intruder who could write there, would not alert.
One match, the test's value. Reading the rule first is what lets the defender say, at the next step, whether an alert or its absence is the rule doing its job.
A missing alert for a record the rule matches is a scheduling or deployment problem; a missing alert for a record the rule does not match is a gap in the rule. The two need different fixes, and only the rule's text tells them apart.
Step 5, the alert
Whether it fired, and how long afterThe alerts in the twenty minutes after the test:
SecurityAlert
| where TimeGenerated between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:20:00Z))
| project TimeGenerated, AlertName, CompromisedEntity
One alert, seven minutes after the test started:
Time Alert Host
-------- -------------------------------------------------------- ----------
10:07:08 PU-R03 Run key added for an executable outside Progra... NE-PT-WS01
The alert arrived after the cleanup. By 10:07 the value was already gone, and an analyst who went to the workstation to look at the Run key would have found nothing there.
That is normal for a test with a quick cleanup and worth knowing about: the evidence is in the records, not on the host, and an analyst who checks only the live host will conclude the alert was false.
PU-R03 fired at 10:07:08 on the workstation. Seven minutes is the rule's five-minute schedule plus the time for the record to arrive, which is the expected delay for this rule on this range. The alert names the host and the registry value, which is enough for an analyst to find the record and, with the test log, to find the test.
The alert's severity is medium, which affects how quickly the queue reaches it but not whether it is real. An analyst's queue on a busy morning is sorted by severity, and a medium alert can wait an hour behind high ones. The plan for an exercise says which severities it expects for each test, so that a long wait for a medium alert is not mistaken for a missed detection.
The test has now reached the fourth and fifth measures: detected, and the alert is about the real activity. A detection test would stop here and report a pass. The course reads one step further.
Step 6, the verdict
What the defense decided, and on what basisThe incident behind the alert, with its classification and the analyst's comment:
SecurityIncident
| where CreatedTime between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:20:00Z))
| project CreatedTime, ClosedTime, Classification, ClassificationComment
One incident, opened with the alert and closed twenty-four minutes later:
Created Closed Classification Comment
-------- -------- -------------- --------------------------------------------------
10:07:08 10:31:08 BenignPositive Value name is Atomic Red Team. Purple team test...
The incident was opened with the alert and closed twenty-four minutes later, which is a reasonable time for a medium-severity alert on a weekday morning. The speed is not the finding. The finding is in the comment, which records what the analyst knew and how they decided, and it is the most useful field in the incident for a purple team.
The analyst closed it as benign, writing that the value was named Atomic Red Team and that purple team testing was running on the host that week. The conclusion was right: the activity was the test.
The basis was weak: the analyst did not match the alert to PT-0104's row, but reasoned that a test was likely. The same reasoning would have closed a real intruder's Run key that week, had one arrived with a less honest name.
It would be easy to read the analyst's verdict as a mistake, and it is not quite that. The team had announced the testing, the value was named after the tool, and the conclusion was correct.
The problem is that the method would have reached the same conclusion for anything that looked vaguely like a test, and the course's later modules show a case where it did exactly that for an alert that was not what the analyst assumed.
That is the lesson's finding, and it is not about the rule. The defense needs a way for analysts to confirm an alert against the test log, and the course's later modules build one: exercise incidents are closed only after matching the test's ID, and the comment names it.
Classify it
PT-0104 on the seven measuresWith all six steps read, the test can be classified:
The evidence for each measure, so the classification can be checked line by line:
PT-0104 reached all seven measures, which makes it the best result of the engagement's first morning, and the seventh is amber rather than green. Classification always names the last measure reached and any finding attached to it, not a score.
PT-0104 is "detected, with a finding on the verdict's basis"; PT-0103 in Lesson 0.1 was "alerted, not handled". The two sound similar and send work to the same owner, the SOC lead, for different reasons, and the classification has to say which.
A verdict that is right by luck is a finding in its own right: it will be wrong the first time luck runs out. The classification in the course's terms is detected, with a process finding on the verdict's basis.
The reading, written down
What the record of a test looks likeThe six steps produce a short record that anyone can check by rerunning its queries:
Writing the reading down at the end of each test, rather than at the very end of the day, is the habit that keeps it accurate. By the afternoon of an exercise the morning's tests blur together, and the details that decide a classification, which comment, which minute, are the first to go.
This is the evidence register's first row, in the form Lesson 10.3 uses for a whole exercise. The card is also short enough to be compared with another test's.
Put PT-0103's card from Lesson 0.1 beside it and the difference between the two results is visible in one line each: an alert with no incident, and an incident with a weak verdict. Comparisons like that, across many tests, are how a purple team sees a pattern rather than a series of anecdotes.
Every line names its record, so a reader who doubts one reruns its query rather than arguing with the writer. Most of what the course teaches about reporting is already in this one card.
Validate
The verdict by its label, and by its basisThe obvious way to read the verdict is by its classification:
SecurityIncident
| where CreatedTime between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:20:00Z))
| project CreatedTime, ClosedTime, Classification,
ClassificationCommentSecurityIncident
| where CreatedTime between (datetime(2026-09-15T10:00:00Z)
.. datetime(2026-09-15T10:20:00Z))
| extend NamesTest = ClassificationComment has "PT-0104"
| project CreatedTime, Classification, NamesTestThe refined read is a single extra condition, and it turns a pass into a finding. On the chain both seats use, PT-0104 reads:
Outcome chain
PT-0104What PT-0104 shows
Agreed by both seats.
- A clean run with a complete cleanup.
- Recorded, matched, alerted and investigated.
- A right verdict on a basis that would close an intrusion too.
Detected end to end, with a finding in the last step.
The chain's plan row carries the operator's variant forward, so the reading ends with the next test already named. A purple team that reads a test well and plans nothing from it has learned something about yesterday's defense; one that plans the variant has started on tomorrow's.
Every later lesson reads its tests the same way, and most of the course's findings appear at the steps a detection test never reaches.
Adapt: four verdicts the same alert could have had
And which ones the course acceptsThe same alert could have been closed four ways, and the course treats them very differently:
True positive, naming the test
Benign positive, "testing this week"
Benign positive, matched to the row
False positive
The difference between the second and third rows is invisible in a count of verdicts and obvious in the comments. Both are benign positives; one was checked and one was assumed. A program that measures its SOC by the labels alone will never see it, which is why the course reads the comments on every exercise incident.
The first is what the course asks for on an exercise day: the activity was real, so the incident is a true positive, and the comment names the test that caused it. The third reaches the right conclusion by the right route under another label.
The second is what happened here, and the fourth would be a rule firing on the wrong activity. Module 8 of the course is largely about getting every exercise verdict into the first row.
Read your first test
The case below has you work PT-0104 in the console in the course's order: the operator's row, the registry records, the alert, and the verdict. Work in the platform's Advanced hunting console with the Purple Range selected.
When you have the four answers, write the reading as section 9 did, in six short lines, before moving on to the next lesson, and keep it where you will keep the rest of your course notes, because Lesson 0.6 asks you to look back at it. It takes about a minute, and it is the same card you will write for every test in the whole course.
The last answer is the one to carry forward: a verdict is judged by its basis as well as its label, and the basis is in the comment.
Practice
Read PT-0105 in six steps your labThe practice is in the console, against the recording; your own lab is built in Lessons 0.7 to 0.11.
Operator: step 1
PT-0105's row: host, account, time and cleanup.
Defender: steps 2 and 3
Its process records, its service record, and the cleanup.
Defender: step 4
The rule meant for it, and whether it should match.
Defender: steps 5 and 6
The alert and the incident, with the comment.
Both: the card
Six lines, as section 9 wrote them for PT-0104.
What you should end up with: A six-line card for PT-0105, each line traceable to a query.
Resources
Publishers revise these pages; where a page and this lesson disagree, the page is newer.