Advanced KQL: Building Enterprise Hunting Queries
The first query found a suspicious sign-in.
The second query found PowerShell. The third query found an email. The fourth query found a cloud application event.
Each query was useful, but none of them told the whole story.
Agent Foskett knew enterprise hunting was different. In a large environment, the best investigations do not stop at one table. They connect identity, endpoint, email and cloud evidence into one scalable hunting workflow.
Lesson overview
Learn how to build enterprise-scale hunting queries that correlate Microsoft Defender XDR telemetry across identity, endpoint, email, cloud applications and alert evidence.
Why enterprise hunting queries matter
The enterprise hunting mindset
Step 1 — Define the investigation scope
- 1
- 2
- 3
- 4
- 5
- 6
- 7
let Lookback = 14d; let TargetUser = "user@contoso.com"; let TargetDevice = "device01.contoso.com"; let StartTime = ago(Lookback); print StartTime = StartTime, EndTime = now(), TargetUser = TargetUser, TargetDevice = TargetDevice
Step 2 — Build an identity evidence set
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let Lookback = 14d;
let TargetUser = "user@contoso.com";
let IdentityEvidence =
IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where AccountUpn =~ TargetUser
| project Timestamp,
Source = "IdentityLogonEvents",
Account = AccountUpn,
DeviceName,
EvidenceType = ActionType,
EvidenceDetail = strcat(IPAddress, " ", Location, " ", FailureReason);
IdentityEvidence
| order by Timestamp ascStep 3 — Add endpoint process evidence
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
let Lookback = 14d;
let TargetDevice = "device01.contoso.com";
let EndpointEvidence =
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where DeviceName =~ TargetDevice
| where FileName in~ ("powershell.exe", "cmd.exe", "rundll32.exe", "mshta.exe")
or ProcessCommandLine has_any ("-enc", "downloadstring", "iex", "http", "bypass")
| project Timestamp,
Source = "DeviceProcessEvents",
Account = InitiatingProcessAccountName,
DeviceName,
EvidenceType = FileName,
EvidenceDetail = ProcessCommandLine;
EndpointEvidence
| order by Timestamp ascStep 4 — Add email evidence
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
let Lookback = 14d;
let TargetUser = "user@contoso.com";
let EmailEvidence =
EmailEvents
| where Timestamp > ago(Lookback)
| where RecipientEmailAddress =~ TargetUser
| where ThreatTypes has_any ("Phish", "Malware", "Spam")
| project Timestamp,
Source = "EmailEvents",
Account = RecipientEmailAddress,
DeviceName = "",
EvidenceType = DeliveryAction,
EvidenceDetail = strcat(SenderFromAddress, " | ", Subject, " | ", NetworkMessageId);
EmailEvidence
| order by Timestamp ascStep 5 — Correlate email clicks
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let Lookback = 14d;
let TargetUser = "user@contoso.com";
EmailEvents
| where Timestamp > ago(Lookback)
| where RecipientEmailAddress =~ TargetUser
| project NetworkMessageId, EmailTime = Timestamp, SenderFromAddress, Subject
| join kind=leftouter (
UrlClickEvents
| where Timestamp > ago(Lookback)
| where AccountUpn =~ TargetUser
| project NetworkMessageId, ClickTime = Timestamp, Url, ActionType, IsClickedThrough
) on NetworkMessageId
| project EmailTime, ClickTime, AccountUpn = TargetUser, SenderFromAddress, Subject, Url, ActionType, IsClickedThrough
| order by EmailTime ascStep 6 — Add cloud application activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let Lookback = 14d;
let TargetUser = "user@contoso.com";
let CloudEvidence =
CloudAppEvents
| where Timestamp > ago(Lookback)
| where AccountId =~ TargetUser or AccountDisplayName =~ TargetUser
| project Timestamp,
Source = "CloudAppEvents",
Account = AccountDisplayName,
DeviceName = tostring(RawEventData.DeviceName),
EvidenceType = ActionType,
EvidenceDetail = strcat(Application, " | ", ObjectName, " | ", IPAddress);
CloudEvidence
| order by Timestamp ascStep 7 — Build a combined enterprise 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
let Lookback = 14d;
let TargetUser = "user@contoso.com";
let IdentityEvidence = IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where AccountUpn =~ TargetUser
| project Timestamp, Source="IdentityLogonEvents", Account=AccountUpn, DeviceName, EvidenceType=ActionType, EvidenceDetail=strcat(IPAddress, " ", Location);
let EmailEvidence = EmailEvents
| where Timestamp > ago(Lookback)
| where RecipientEmailAddress =~ TargetUser
| project Timestamp, Source="EmailEvents", Account=RecipientEmailAddress, DeviceName="", EvidenceType=DeliveryAction, EvidenceDetail=strcat(SenderFromAddress, " | ", Subject);
let TargetDevice = "device01.contoso.com";
let EndpointEvidence = DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where DeviceName =~ TargetDevice
| where FileName in~ ("powershell.exe", "cmd.exe", "rundll32.exe", "mshta.exe")
| project Timestamp, Source="DeviceProcessEvents", Account=InitiatingProcessAccountName, DeviceName, EvidenceType=FileName, EvidenceDetail=ProcessCommandLine;
let CloudEvidence = CloudAppEvents
| where Timestamp > ago(Lookback)
| where AccountId =~ TargetUser or AccountDisplayName =~ TargetUser
| project Timestamp, Source="CloudAppEvents", Account=AccountId, DeviceName="", EvidenceType=ActionType, EvidenceDetail=strcat(Application, " | ", ObjectName);
union IdentityEvidence, EmailEvidence, EndpointEvidence, CloudEvidence
| order by Timestamp ascStep 8 — Join alerts to evidence
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
let Lookback = 14d;
AlertInfo
| where Timestamp > ago(Lookback)
| project AlertId, AlertTime = Timestamp, Title, Severity, Category
| join kind=leftouter (
AlertEvidence
| where Timestamp > ago(Lookback)
| project AlertId, EntityType, EvidenceRole, DeviceName, AccountName, FileName, RemoteUrl
) on AlertId
| project AlertTime, Title, Severity, Category, EntityType, EvidenceRole, DeviceName, AccountName, FileName, RemoteUrl
| order by AlertTime descStep 9 — Summarise enterprise exposure
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let Lookback = 30d;
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("powershell.exe", "cmd.exe", "rundll32.exe", "mshta.exe")
| where ProcessCommandLine has_any ("-enc", "downloadstring", "iex", "http", "bypass", "hidden")
| summarize Events = count(),
Devices = dcount(DeviceName),
Users = dcount(InitiatingProcessAccountName),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp),
ExampleCommand = any(ProcessCommandLine)
by FileName
| order by Events descStep 10 — Build a reusable enterprise hunting query
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
let Lookback = 14d;
// Example-only heuristic: geography and MFA failures need tenant-specific baselines.
let SuspiciousUsers =
IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where Location !in ("Australia", "New Zealand") or FailureReason has "MFA"
| summarize IdentityEvents=count(), FirstIdentity=min(Timestamp), LastIdentity=max(Timestamp) by AccountUpn;
let SuspiciousProcesses =
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where ProcessCommandLine has_any ("-enc", "downloadstring", "iex", "bypass")
| summarize ProcessEvents=count(), Devices=dcount(DeviceName), ExampleCommand=any(ProcessCommandLine) by Account = InitiatingProcessAccountName;
SuspiciousUsers
| join kind=leftouter SuspiciousProcesses on $left.AccountUpn == $right.Account
| extend TotalSignal = IdentityEvents + coalesce(ProcessEvents, 0)
| project AccountUpn, TotalSignal, IdentityEvents, ProcessEvents, Devices, FirstIdentity, LastIdentity, ExampleCommand
| order by TotalSignal desc