In this section
TH1.3 Collection: Iterative Querying
Hunting is iterative
A detection rule is a single query that fires when conditions match. A hunt is five to fifteen queries, each informed by the results of the previous one, converging on a finding or confirming absence.
The pattern is consistent across every campaign in this course:
// Step 1: Orientation — how much sign-in data are we working with?
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0 // Successful sign-ins only
| summarize
TotalSignIns = count(),
UniqueUsers = dcount(UserPrincipalName),
UniqueIPs = dcount(IPAddress),
UniqueCountries = dcount(
tostring(LocationDetails.countryOrRegion))
// Understand the data volume before narrowing
// If TotalSignIns is 350,000 — you know results of 50 are a small fraction// Step 2: Indicator — users with new IPs not in their 30-day baseline
let baseline = SigninLogs
| where TimeGenerated between (ago(37d) .. ago(7d))
| where ResultType == 0
| summarize KnownIPs = make_set(IPAddress, 50)
by UserPrincipalName;
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| join kind=inner baseline on UserPrincipalName
| where not(IPAddress in (KnownIPs))
| summarize
NewIPCount = dcount(IPAddress),
NewIPs = make_set(IPAddress, 5),
Countries = make_set(
tostring(LocationDetails.countryOrRegion), 5)
by UserPrincipalName
// Result: maybe 30 users with sign-ins from new IPs
// Most will be legitimate (VPN changes, travel, new devices)// Step 3: Enrichment — add context to narrow further
// From the 30 users above, which also had a new MFA method
// registered in the same window?
let suspectUsers = SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
// ... (same filter as step 2 producing the 30 users)
| distinct UserPrincipalName;
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName has "User registered security info"
| where InitiatedBy.user.userPrincipalName in (suspectUsers)
// New IP + new MFA method in the same week = elevated concern
// Result: maybe 3 users// Step 4: Pivot — what did these 3 users do?
let confirmedSuspect = datatable(UPN:string)
["j.morrison@northgateeng.com"]; // Example
union
(CloudAppEvents
| where TimeGenerated > ago(7d)
| where AccountId == "j.morrison@northgateeng.com"
| project TimeGenerated, ActionType, Application,
RawEventData),
(EmailEvents
| where TimeGenerated > ago(7d)
| where SenderFromAddress == "j.morrison@northgateeng.com"
| project TimeGenerated, Subject, RecipientEmailAddress)
// Full activity timeline for the suspect account
// Looking for: inbox rules, email forwarding, file access,
// internal phishing from the compromised accountTry it yourself
Exercise: Run the four-step funnel
Using SigninLogs in your environment, execute the four-step pattern above:
Step 1: Run the orientation query. Record the total sign-in volume, unique users, and unique countries for the last 7 days.
Step 2: Run the baseline comparison. How many users have sign-ins from IPs not in their 30-day baseline? This number is your initial result set.
Step 3: For the users identified in step 2, check AuditLogs for new MFA method registrations in the same window. How many overlap?
Step 4: If any users overlap, examine their full activity timeline. What did they do from the new IP?
Document each query, the result count, and your assessment. This is your first hunt record. If you found something suspicious — congratulations, you have a finding. If you found nothing — you have a documented negative finding and the beginning of a detection rule (step 2's query, deployed as a scheduled analytics rule).
The myth: Write a query, run it, check the results. If nothing comes back, the hunt is done.
The reality: A single query tests a single aspect of the hypothesis under a single set of parameters. The attacker who used a slightly different method (Graph API instead of PowerShell), operated in a slightly different time window, or targeted a slightly different data path is missed by a single query. Hunting is iterative specifically because attack techniques have variants. The multi-step funnel — orientation, indicator, enrichment, pivot — is designed to catch variants that a single query would miss and to build the contextual understanding that a single result set cannot provide.
Extend this approach
The iterative pattern described here uses manual execution — you run each query, review results, and decide the next query. For hunts you run repeatedly (monthly cadence hunts from TH14), consider using Sentinel notebooks (Jupyter + MSTICPy) to chain the queries programmatically. The notebook executes the full funnel in sequence, presenting results at each stage for analyst review. TH16 covers notebook-based hunting. For initial learning and for hunts you run for the first time, manual iteration is preferred — you learn more about the data by examining each intermediate result than by running the full chain at once.
References Used in This Subsection
- Microsoft. "Advanced Hunting — Query Best Practices." Microsoft Learn. https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-best-practices
- Microsoft. "Advanced Hunting — Quotas and Usage Parameters." Microsoft Learn. https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-limits
You write a complex KQL hunt query that runs for 3 minutes and returns 50,000 rows. The query is technically correct but operationally unusable. Do you refine the query or adjust the hypothesis?
Both. A 3-minute query with 50,000 rows indicates: the hypothesis is too broad (the query is not specific enough to isolate suspicious activity from normal activity), or the time window is too large, or the filter criteria are too permissive. Refine: add filters that exclude known-good patterns, narrow the time window, or aggregate the results to identify statistical anomalies within the 50,000 rows rather than examining each row individually. A hunt query that returns < 100 rows of genuinely anomalous activity is more valuable than 50,000 rows that require manual review.
You understand the detection gap and the hunt cycle.
TH0 showed you what detection rules fundamentally cannot catch. TH1 gave you the hypothesis-driven methodology that closes that gap. Now you run the hunts.
- 10 complete hunt campaigns — from hypothesis through KQL execution through finding disposition, each campaign based on a real TTP
- 70 production hunt queries — every one mapped to MITRE ATT&CK and tested against realistic telemetry
- Advanced KQL for hunting — UEBA composite risk scoring, retroactive IOC sweeps, and hunt management metrics
- Hypothesis-Driven Hunt Toolkit lab pack — 30 days of realistic M365 and endpoint telemetry with multiple attack patterns seeded in
- TH16 — Scaling hunts across a team — the operating model for a production hunt program