Lab Tool

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.

Note: Every query here is a starting point, not a finished detection. Table/column availability depends on your licensing (Defender for Endpoint, Defender for Office 365, Defender for Identity, or Sentinel with the matching connectors), and thresholds worth tuning (lookback windows, time gaps, command-line substrings) are called out in each description.

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 desc

OAuth 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 desc

Impossible 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, GapMinutes

LSASS 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 desc

New 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 desc

PowerShell 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 desc

Lateral 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