KQL Hunting Library
Real, runnable Advanced Hunting queries for common threat-hunting scenarios across identity, endpoint, and email. Unlike the KQL generated by the Query Builder or the phish-report analyzer, these are hand-written, not IOC-driven.
Mass Mailbox Rule Creation
Attacker-created inbox rules that auto-forward or hide replies, a common post-compromise BEC move to intercept payment threads without the mailbox owner noticing.
CloudAppEvents
| where Timestamp > ago(14d)
| where ActionType in ("New-InboxRule", "Set-InboxRule")
| extend RuleName = tostring(RawEventData.Parameters.Name)
| extend Redirects = tostring(RawEventData.Parameters.ForwardTo), Deletes = tostring(RawEventData.Parameters.DeleteMessage)
| where isnotempty(Redirects) or Deletes == "True"
| project Timestamp, AccountId, RuleName, Redirects, Deletes
| sort by Timestamp descOAuth App Granted Mailbox Access
A third-party OAuth app consented to read/send mail. This is the mechanism behind most "consent phishing," which survives a password reset since no credential was ever stolen.
CloudAppEvents
| where Timestamp > ago(30d)
| where ActionType == "Consent to application."
| extend AppName = tostring(RawEventData.ExtendedProperties[0].Value)
| extend Scopes = tostring(RawEventData.ExtendedProperties[1].Value)
| where Scopes has_any ("Mail.Read", "Mail.Send", "Mail.ReadWrite")
| project Timestamp, AccountId, AppName, Scopes
| sort by Timestamp descImpossible Travel on a Single Account
Two sign-ins from the same account, far enough apart geographically that the same person could not plausibly have made both. A strong credential-compromise signal, not just a VPN false positive, if the gap is large enough.
IdentityLogonEvents
| where Timestamp > ago(1d)
| where ActionType == "LogonSuccess"
| project Timestamp, AccountUpn, Country = tostring(parse_json(AdditionalFields).Country)
| where isnotempty(Country)
| sort by AccountUpn, Timestamp asc
| serialize
| extend PrevCountry = prev(Country), PrevTime = prev(Timestamp), PrevAccount = prev(AccountUpn)
| where AccountUpn == PrevAccount and Country != PrevCountry
| extend GapMinutes = datetime_diff("minute", Timestamp, PrevTime)
| where GapMinutes < 120
| project AccountUpn, PrevCountry, Country, PrevTime, Timestamp, GapMinutesLSASS Memory Access (Credential Dumping)
A non-system process opening lsass.exe with an access mask consistent with reading its memory, the standard precursor to tools like Mimikatz pulling cached credentials.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName has_any ("procdump.exe", "procdump64.exe", "rundll32.exe", "taskmgr.exe")
| where ProcessCommandLine has "lsass"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName
| sort by Timestamp descNew Service Created for Persistence
A new Windows service created outside a maintenance window, a common persistence mechanism, especially when the binary path points somewhere unusual like %TEMP% or a user profile.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName =~ "sc.exe" and ProcessCommandLine has "create"
| extend BinaryPath = extract("binpath[= ]+\"?([^\"]+)", 1, ProcessCommandLine)
| where BinaryPath has_any ("\\Temp\\", "\\AppData\\", "\\Users\\Public\\")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, BinaryPath, InitiatingProcessFileName
| sort by Timestamp descPowerShell with Encoded or Obfuscated Commands
PowerShell invoked with -EncodedCommand or heavy string manipulation (backtick/char-code obfuscation). Legitimate scripts rarely need either, and both are staples of living-off-the-land payload delivery.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "-encodedcommand", "-e ", "FromBase64String", "IEX(", "Invoke-Expression")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileName
| sort by Timestamp descLateral Movement via PsExec / Remote WMI
A remote service or WMI process creation on a device shortly after an inbound SMB/RPC connection from another host: the network and process pairing PsExec-style tooling leaves behind.
let RemoteExec = DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~ ("services.exe", "wmiprvse.exe")
| where FileName !in~ ("svchost.exe");
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "InboundConnectionAccepted" and RemotePort in (135, 445)
| project ConnTime = Timestamp, DeviceName, RemoteIP
| join kind=inner (RemoteExec | project ExecTime = Timestamp, DeviceName, FileName, ProcessCommandLine) on DeviceName
| where ExecTime between (ConnTime .. ConnTime + 2m)
| project DeviceName, RemoteIP, ConnTime, ExecTime, FileName, ProcessCommandLine
| sort by ExecTime desc