Microsoft Defender KQL Threat Hunting Complete Guide
This is the practical hub for Microsoft Defender KQL threat hunting.
It brings together real-world investigation workflows across email security, DMARC failures, URL click activity, identity pivots, endpoint behaviour and cloud activity.
The goal is simple: move beyond alert chasing and use KQL to prove what happened, who was affected, whether the activity was delivered or clicked, and what to check next.
Complete guide summary
A central Microsoft Defender KQL threat hunting guide for analysts, administrators and business owners who want to understand what the security data is really showing.
What is Microsoft Defender KQL threat hunting?
How to read these KQL queries
Email threat hunting: sender mismatch and spoofing
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
EmailEvents
| where Timestamp > ago(30d)
| where SenderFromDomain != SenderMailFromDomain
| project
Timestamp,
SenderFromAddress,
SenderFromDomain,
SenderMailFromAddress,
SenderMailFromDomain,
RecipientEmailAddress,
Subject,
AuthenticationDetails,
DeliveryAction,
NetworkMessageId
| order by Timestamp desc
π Deep dive β Microsoft Defender email spoofing detection with KQL.
DMARC failure analysis
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
EmailEvents
| where Timestamp > ago(30d)
| where AuthenticationDetails has "dmarc=fail"
| project
Timestamp,
SenderFromAddress,
SenderFromDomain,
SenderMailFromDomain,
RecipientEmailAddress,
Subject,
AuthenticationDetails,
DeliveryAction,
NetworkMessageId
| order by Timestamp desc
π Deep dive β DMARC failures explained with KQL.
Pivot from suspicious email to URL clicks
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let SuspiciousMessages =
EmailEvents
| where Timestamp > ago(30d)
| where AuthenticationDetails has_any ("dmarc=fail", "spf=fail", "dkim=fail", "spoof")
| project NetworkMessageId, EmailTime = Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, DeliveryAction;
SuspiciousMessages
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(30d)
| project NetworkMessageId, ClickTime = Timestamp, AccountUpn, Url, ActionType
) on NetworkMessageId
| order by ClickTime desc
Identity threat hunting after a click
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
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
π Related guide β Using impossible travel sign-ins in investigations.
Endpoint threat hunting: suspicious PowerShell
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
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
Cloud activity threat hunting: after-hours file access
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
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
The real investigation flow
Continue your investigation
Related case studies β The email came from meβ¦ but I never sent it and MFA session hijacking.
