Microsoft Defender KQL Threat Hunting Guide
Nothing triggered an alert.
But the data already knew.
This guide brings together practical Microsoft Defender XDR hunting examples using KQL, EmailEvents, AuthenticationDetails, spoofed domain checks, UrlClickEvents and identity pivots.
It is designed for real-world investigation, not just theory.
At GEMXIT, we use KQL to move beyond alert chasing and into behaviour-based investigation across security operations, Microsoft Defender, identity and access, and broader Microsoft Security posture.
Guide summary
This page is a practical KQL threat hunting hub for Microsoft Defender XDR. It focuses on the exact investigation paths analysts use when something feels wrong but no obvious alert explains it.
Why this guide exists
Start with failed email authentication
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
EmailEvents | where Timestamp > ago(30d) | where AuthenticationDetails has_any ("spf=fail", "dkim=fail", "dmarc=fail") | project Timestamp, SenderFromAddress, SenderFromDomain, SenderMailFromAddress, SenderMailFromDomain, RecipientEmailAddress, Subject, AuthenticationDetails, ThreatTypes, DeliveryAction | order by Timestamp desc
Hunt for spoofed domain signals
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
EmailEvents | where Timestamp > ago(30d) | where AuthenticationDetails has "spoof" or AuthenticationDetails has "dmarc=fail" | project Timestamp, SenderFromAddress, SenderFromDomain, SenderMailFromDomain, RecipientEmailAddress, Subject, AuthenticationDetails, DeliveryAction, NetworkMessageId | order by Timestamp desc
Compare visible sender and envelope sender
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
EmailEvents | where Timestamp > ago(30d) | where SenderFromDomain != SenderMailFromDomain | project Timestamp, SenderFromAddress, SenderFromDomain, SenderMailFromAddress, SenderMailFromDomain, RecipientEmailAddress, Subject, AuthenticationDetails, DeliveryAction | order by Timestamp desc
Find repeated spoofing or failed-authentication campaigns
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
EmailEvents | where Timestamp > ago(30d) | where AuthenticationDetails has_any ("spf=fail", "dkim=fail", "dmarc=fail", "spoof") | summarize MessageCount = count(), RecipientCount = dcount(RecipientEmailAddress) by SenderFromAddress, SenderFromDomain, Subject, DeliveryAction | where MessageCount >= 3 | order by MessageCount desc
Pivot from suspicious email to user clicks
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let SuspiciousMessages = EmailEvents | where Timestamp > ago(30d) | where AuthenticationDetails has_any ("spf=fail", "dkim=fail", "dmarc=fail", "spoof") | project NetworkMessageId, EmailTime = Timestamp, SenderFromAddress, RecipientEmailAddress, Subject; SuspiciousMessages | join kind=inner ( UrlClickEvents | where Timestamp > ago(30d) | project NetworkMessageId, ClickTime = Timestamp, AccountUpn, Url, ActionType ) on NetworkMessageId | order by ClickTime desc
Pivot from email click to sign-in activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
let ClickedUsers = UrlClickEvents | where Timestamp > ago(7d) | where ActionType has_any ("ClickAllowed", "UrlScanInProgress") | project AccountUpn, ClickTime = Timestamp, Url; ClickedUsers | join kind=inner ( IdentityLogonEvents | where Timestamp > ago(7d) | project AccountUpn, SignInTime = Timestamp, IPAddress, Location, Application, LogonType ) on AccountUpn | where SignInTime between ((ClickTime - 2h) .. (ClickTime + 6h)) | order by ClickTime desc
Hunt for after-hours file activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
CloudAppEvents | where Timestamp > ago(14d) | where ActionType has_any ("FileDownloaded", "FileAccessed") | extend HourOfDay = datetime_part("hour", Timestamp) | where HourOfDay < 6 or HourOfDay > 20 | project Timestamp, AccountDisplayName, AccountId, IPAddress, Application, ActionType, ObjectName | order by Timestamp desc
Hunt for suspicious PowerShell behaviour
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
DeviceProcessEvents | where Timestamp > ago(14d) | where FileName in~ ("powershell.exe", "pwsh.exe") | where ProcessCommandLine has_any ( "-enc", "-encodedcommand", "iex", "downloadstring", "invoke-webrequest", "frombase64string", "hidden" ) | project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName | order by Timestamp desc
What good KQL threat hunting looks like
Related Agent Foskett investigations
Final thought
If you want help improving Microsoft Defender hunting, alert investigation and security visibility, 👉 review Microsoft Defender services
Develop IT. Protect IT. GEMXIT PTY LTD | GEMXIT UK LTD