Advanced KQL: Building a Complete Hunting Workbook.
One alert was not enough.
The sign-in looked suspicious, the endpoint showed strange process activity, the email trail raised more questions, and the cloud activity did not line up with the user's normal behaviour.
Agent Foskett realised the answer was not hidden in one query. It was spread across identity, endpoint, email, cloud and network telemetry.
The investigation needed more than a hunt. It needed a complete hunting workbook that could tell the full story.
Lesson overview
Learn how to combine advanced KQL techniques into a complete Microsoft Defender XDR hunting workbook for enterprise-scale investigations.
Why a complete hunting workbook matters
The complete workbook mindset
Workbook section 1 — Investigation parameters
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
let Lookback = 7d;
let InvestigationUser = "user@contoso.com";
let InvestigationDevice = "device01.contoso.com";
let InvestigationSender = "sender@example.com";
let InvestigationIP = "203.0.113.10";
let StartTime = ago(Lookback);
let EndTime = now();
print
Lookback = Lookback,
InvestigationUser = InvestigationUser,
InvestigationDevice = InvestigationDevice,
InvestigationSender = InvestigationSender,
InvestigationIP = InvestigationIP,
StartTime = StartTime,
EndTime = EndTimeWorkbook section 2 — Identity investigation summary
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
IdentityLogonEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountUpn =~ InvestigationUser or IPAddress == InvestigationIP
| summarize
LogonEvents = count(),
SuccessfulLogons = countif(ActionType has "LogonSuccess"),
FailedLogons = countif(ActionType has "LogonFailed"),
Locations = make_set(Location, 10),
IPAddresses = make_set(IPAddress, 20),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by AccountUpn
| order by LastSeen descWorkbook section 3 — Identity drilldown timeline
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
IdentityLogonEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountUpn =~ InvestigationUser
| project
Timestamp,
Source = "IdentityLogonEvents",
Entity = AccountUpn,
Activity = ActionType,
IPAddress,
Location,
DeviceName,
FailureReason
| order by Timestamp ascWorkbook section 4 — Email investigation summary
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
EmailEvents
| where Timestamp between (StartTime .. EndTime)
| where RecipientEmailAddress =~ InvestigationUser
or SenderFromAddress =~ InvestigationSender
or SenderMailFromAddress =~ InvestigationSender
| summarize
Messages = count(),
Quarantined = countif(DeliveryAction =~ "Quarantined"),
Delivered = countif(DeliveryAction =~ "Delivered"),
ThreatTypes = make_set(ThreatTypes, 10),
Subjects = make_set(Subject, 10),
NetworkMessageIds = make_set(NetworkMessageId, 20)
by SenderFromAddress, SenderMailFromAddress
| order by Messages descWorkbook section 5 — URL and attachment pivots
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
let RelatedMessages =
EmailEvents
| where Timestamp between (StartTime .. EndTime)
| where RecipientEmailAddress =~ InvestigationUser
or SenderFromAddress =~ InvestigationSender
or SenderMailFromAddress =~ InvestigationSender
| project NetworkMessageId, EmailTimestamp = Timestamp, SenderFromAddress, RecipientEmailAddress, Subject;
RelatedMessages
| join kind=leftouter (
UrlClickEvents
| where Timestamp between (StartTime .. EndTime)
| project NetworkMessageId, UrlClickTimestamp = Timestamp, Url, ActionType, IsClickedThrough
) on NetworkMessageId
| join kind=leftouter (
EmailAttachmentInfo
| where Timestamp between (StartTime .. EndTime)
| project NetworkMessageId, FileName, SHA256, FileType
) on NetworkMessageId
| project EmailTimestamp, SenderFromAddress, RecipientEmailAddress, Subject, Url, ActionType, IsClickedThrough, FileName, SHA256
| order by EmailTimestamp descWorkbook section 6 — Endpoint process investigation
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
DeviceProcessEvents
| where Timestamp between (StartTime .. EndTime)
| where DeviceName =~ InvestigationDevice
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe", "rundll32.exe", "mshta.exe", "certutil.exe", "regsvr32.exe")
or ProcessCommandLine has_any ("-enc", "-encodedcommand", "downloadstring", "iex", "bypass", "hidden")
| project
Timestamp,
DeviceName,
InitiatingProcessAccountName,
FileName,
ProcessCommandLine,
InitiatingProcessFileName,
InitiatingProcessCommandLine
| order by Timestamp ascWorkbook section 7 — Network behaviour and beaconing
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
DeviceNetworkEvents
| where Timestamp between (StartTime .. EndTime)
| where DeviceName =~ InvestigationDevice
| where isnotempty(RemoteUrl) or isnotempty(RemoteIP)
| summarize
Connections = count(),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp),
Processes = make_set(InitiatingProcessFileName, 10),
Ports = make_set(RemotePort, 10)
by DeviceName, RemoteUrl, RemoteIP
| extend DurationMinutes = datetime_diff("minute", LastSeen, FirstSeen)
| order by Connections descWorkbook section 8 — Cloud application activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
CloudAppEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountDisplayName =~ InvestigationUser
or AccountObjectId =~ InvestigationUser
or IPAddress == InvestigationIP
| project
Timestamp,
AccountDisplayName,
Application,
ActionType,
ObjectName,
IPAddress,
UserAgent,
RawEventData
| order by Timestamp ascWorkbook section 9 — Build a unified investigation timeline
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
let IdentityTimeline =
IdentityLogonEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountUpn =~ InvestigationUser
| project Timestamp, Source = "Identity", Entity = AccountUpn, Activity = ActionType, Detail = strcat(IPAddress, " ", Location);
let EmailTimeline =
EmailEvents
| where Timestamp between (StartTime .. EndTime)
| where RecipientEmailAddress =~ InvestigationUser or SenderFromAddress =~ InvestigationSender
| project Timestamp, Source = "Email", Entity = RecipientEmailAddress, Activity = DeliveryAction, Detail = strcat(SenderFromAddress, " | ", Subject);
let EndpointTimeline =
DeviceProcessEvents
| where Timestamp between (StartTime .. EndTime)
| where DeviceName =~ InvestigationDevice
| project Timestamp, Source = "Endpoint", Entity = DeviceName, Activity = FileName, Detail = ProcessCommandLine;
let CloudTimeline =
CloudAppEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountDisplayName =~ InvestigationUser
| project Timestamp, Source = "Cloud", Entity = AccountDisplayName, Activity = ActionType, Detail = strcat(Application, " | ", ObjectName);
union IdentityTimeline, EmailTimeline, EndpointTimeline, CloudTimeline
| order by Timestamp ascWorkbook section 10 — Executive investigation summary
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
let Timeline =
union
(IdentityLogonEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountUpn =~ InvestigationUser
| project Timestamp, Category = "Identity"),
(EmailEvents
| where Timestamp between (StartTime .. EndTime)
| where RecipientEmailAddress =~ InvestigationUser
| project Timestamp, Category = "Email"),
(DeviceProcessEvents
| where Timestamp between (StartTime .. EndTime)
| where DeviceName =~ InvestigationDevice
| project Timestamp, Category = "Endpoint"),
(CloudAppEvents
| where Timestamp between (StartTime .. EndTime)
| where AccountDisplayName =~ InvestigationUser
| project Timestamp, Category = "Cloud");
Timeline
| summarize Events = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by Category
| order by Events descWorkbook design checklist
Common workbook mistakes
Related Agent Foskett Academy lessons
Advanced KQL series milestone
Final thought
Advanced KQL Building a Complete Hunting Workbook in Microsoft Defender XDR
Agent Foskett Academy Lesson 130 teaches defenders how to build a complete Microsoft Defender XDR hunting workbook using advanced KQL, reusable parameters, identity telemetry, endpoint telemetry, email evidence, cloud activity and investigation timelines.
Microsoft Defender XDR hunting workbook tutorial
This lesson explains how to design complete hunting workflows, correlate Defender XDR tables, use reusable KQL sections, build timelines and produce investigation-ready evidence for enterprise security teams.
Enterprise threat hunting workbook with KQL
Security analysts can use advanced KQL techniques such as joins, parse_json(), mv-expand, regular expressions, time series analysis and reusable functions to build scalable hunting workbooks across identity, endpoint, email, cloud and network telemetry.
