KQL Query Reference
Production detection queries for Microsoft Sentinel and Defender XDR, organized by ATT&CK tactic. 39 queries across 11 tactics, each with its source table, technique, and confidence. Copy, adapt, run. No account needed.
These run in Microsoft Sentinel (Logs) and Defender XDR (Advanced Hunting). Tune the time windows and thresholds to your environment before relying on them for alerting. Each query is tagged with its source table, the ATT&CK technique it detects, and a confidence level reflecting how prone it is to false positives.
Credential Access 6
Credential theft is the pivot point of most intrusions: once an attacker has valid credentials, their activity blends into legitimate authentication. These detections target the act of stealing them rather than their later use. Most of the signal is on the endpoint (DeviceProcessEvents) where the dumping tool runs, with the LSASS-access and SAM-hive queries being high-confidence because legitimate software rarely touches those in these specific ways. The cloud-side query catches MFA registration from an unexpected context, which is how attackers make stolen access durable.
DeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName =~ "rundll32.exe"
| where ProcessCommandLine has "comsvcs" and ProcessCommandLine has "MiniDump"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileNameDeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName =~ "procdump.exe" or FileName =~ "procdump64.exe"
| where ProcessCommandLine has_any ("lsass", "-ma")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLineDeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName =~ "reg.exe"
| where ProcessCommandLine has "save" and ProcessCommandLine has_any ("sam", "system", "security")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDeviceProcessEvents
| where Timestamp > ago(7d)
| where ProcessCommandLine has_any ("findstr /si password", "dir /s *pass*", "dir /s *cred*", "Get-ChildItem -Recurse *password*")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDeviceProcessEvents
| where Timestamp > ago(7d)
| where ProcessCommandLine has_any ("Invoke-Kerberoast", "Get-SPNUsers", "Rubeus kerberoast")
| project Timestamp, DeviceName, AccountName, ProcessCommandLineAuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "User registered security info"
| extend UserPrincipalName = tostring(TargetResources[0].userPrincipalName)
| join kind=inner (SigninLogs | where TimeGenerated > ago(24h) | where ResultType == "0") on UserPrincipalName
| where IPAddress != IPAddress1
| project TimeGenerated, UserPrincipalName, OperationName, IPAddress, IPAddress1Initial Access 4
Initial access is where the intrusion crosses your perimeter, so the signal is split between the endpoint (a malicious document or mounted image executing) and the identity plane (a sign-in that should not have succeeded). The endpoint queries look for the classic delivery patterns: Office applications spawning script interpreters, and ISO/VHD containers used to bypass Mark-of-the-Web. The SigninLogs queries catch the cloud equivalents, password spraying and impossible-travel sign-ins, where the attacker is brute-forcing or using stolen credentials from an implausible location.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~ ("winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe")
| where FileName in~ ("powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe")
| summarize Count = count() by DeviceName, AccountName, InitiatingProcessFileName, FileName
| order by Count descDeviceFileEvents
| where Timestamp > ago(7d)
| where FileName endswith_any (".iso", ".vhd", ".vhdx", ".img")
| project DeviceName, FileName, Timestamp, AccountNameSigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == "50126"
| summarize FailedAttempts = count(), TargetAccounts = dcount(UserPrincipalName), SourceIPs = dcount(IPAddress) by bin(TimeGenerated, 10m)
| where FailedAttempts > 20 and TargetAccounts > 5SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == "0"
| summarize Locations = make_set(Location), LocationCount = dcount(Location), IPs = make_set(IPAddress) by UserPrincipalName
| where LocationCount > 1Execution 4
Execution detections catch code running through the mechanisms attackers favour because they blend in: signed LOLBins, script interpreters, and WMI. The common thread across these queries is not the binary itself (powershell.exe, mshta.exe, certutil.exe are all legitimate) but the command line, the parent process, or the network behaviour that betrays malicious intent. Read the ProcessCommandLine field, not just the FileName.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-nop", "-noni", "-w hidden", "-enc", "-EncodedCommand", "bypass", "iex", "invoke-expression", "downloadstring", "Net.WebClient")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName =~ "mshta.exe"
| where ProcessCommandLine has_any ("http://", "https://", "javascript:", "vbscript:")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName in~ ("certutil.exe", "bitsadmin.exe", "mshta.exe")
| where ProcessCommandLine has_any ("http://", "https://", "urlcache", "transfer", "download")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLineDeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName =~ "wmiprvse.exe"
| where FileName !in~ ("csc.exe", "mofcomp.exe")
| summarize Count = count() by DeviceName, FileName, ProcessCommandLine
| where Count < 5Persistence 6
Persistence is how an attacker survives a reboot or a remediation, so it is one of the highest-value things to hunt: finding it often means finding a foothold you did not know about. The mechanisms span the registry (Run keys, service ImagePaths), the filesystem (startup folders), WMI event subscriptions, and, in the cloud, OAuth consent grants. Watch the InitiatingProcessFileName on the endpoint queries: persistence written by a script interpreter rather than an installer is the tell.
DeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName =~ "schtasks.exe" and ProcessCommandLine has "/create"
| where InitiatingProcessFileName in~ ("cmd.exe", "powershell.exe", "wscript.exe", "cscript.exe", "mshta.exe")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDeviceRegistryEvents
| where Timestamp > ago(1h)
| where RegistryKey has @"Services\" and RegistryValueName == "ImagePath"
| where RegistryValueData has_any (@"\temp\", @"\tmp\", @"\appdata\", @"\users\public\")
| project Timestamp, DeviceName, AccountName, RegistryKey, RegistryValueDataDeviceRegistryEvents
| where Timestamp > ago(7d)
| where RegistryKey has_any (@"CurrentVersion\Run", @"CurrentVersion\RunOnce")
| where ActionType in ("RegistryValueSet", "RegistryKeyCreated")
| project Timestamp, DeviceName, AccountName, RegistryKey, RegistryValueName, RegistryValueData, InitiatingProcessFileNameDeviceEvents
| where Timestamp > ago(1h)
| where ActionType == "WmiBindEventFilterToConsumer"
| project Timestamp, DeviceName, AccountName, ActionType, InitiatingProcessFileNameDeviceFileEvents
| where Timestamp > ago(7d)
| where FolderPath has @"Start Menu\Programs\Startup"
| project Timestamp, DeviceName, AccountName, FileName, FolderPath, InitiatingProcessFileNameAuditLogs
| where TimeGenerated > ago(7d)
| where OperationName == "Consent to application"
| extend AppName = tostring(TargetResources[0].displayName)
| extend Permissions = tostring(TargetResources[0].modifiedProperties)
| where Permissions has_any ("Mail.Read", "Mail.ReadWrite", "Files.ReadWrite.All", "User.ReadWrite.All")
| project TimeGenerated, UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName), AppName, PermissionsLateral Movement 4
Lateral movement is the attacker spreading from the first compromised host to others, and it is where containment decisions get made: every new host in the chain widens the incident. These queries catch the common techniques, PsExec service installs, pass-the-hash (NTLM NewCredentials logons), SMB copies to admin shares, and RDP from unexpected sources. The logon-based queries (DeviceLogonEvents) are where the cross-host story assembles: follow the RemoteIP and account across hosts.
DeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName =~ "psexesvc.exe" or (FileName =~ "services.exe" and ProcessCommandLine has "PSEXESVC")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLineDeviceLogonEvents
| where Timestamp > ago(1h)
| where LogonType == "NewCredentials"
| where Protocol == "NTLM"
| where InitiatingProcessFileName !in ("services.exe", "lsass.exe")
| project Timestamp, DeviceName, AccountName, RemoteIP, Protocol, InitiatingProcessFileNameDeviceFileEvents
| where Timestamp > ago(7d)
| where FolderPath has_any (@"\ADMIN$\", @"\C$\")
| where FileName endswith_any (".exe", ".dll", ".bat", ".ps1")
| project Timestamp, DeviceName, AccountName, FileName, FolderPathDeviceLogonEvents
| where Timestamp > ago(1h)
| where LogonType == "RemoteInteractive"
| where AccountName !endswith "$"
| project Timestamp, DeviceName, AccountName, RemoteIP, RemoteDeviceNameDefense Evasion 6
Defense evasion is the attacker actively working against your visibility, which makes these among the most important detections to get right: a successful evasion blinds your other rules. The queries here catch tampering with the security stack itself, AMSI bypasses, ETW provider tampering, Defender exclusion changes, event log clearing, and process masquerading (a system binary name running from the wrong path). Several are high-confidence precisely because there is no legitimate reason to perform them.
DeviceProcessEvents
| where Timestamp > ago(1h)
| where ProcessCommandLine has_any ("AmsiScanBuffer", "amsiInitFailed", "AmsiUtils", "amsi.dll")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLineDeviceProcessEvents
| where Timestamp > ago(1h)
| where ProcessCommandLine has_any ("logman stop", "logman delete", "Remove-EtwTraceProvider", "Set-EtwTraceProvider")
| project Timestamp, DeviceName, AccountName, ProcessCommandLineDeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("svchost.exe", "csrss.exe", "lsass.exe", "services.exe")
| where not(FolderPath has @"C:\Windows\System32" or FolderPath has @"C:\Windows\SysWOW64")
| project Timestamp, DeviceName, FileName, FolderPath, SHA256DeviceProcessEvents
| where Timestamp > ago(7d)
| where ProcessCommandLine has_any ("wevtutil cl", "Clear-EventLog", "Remove-EventLog")
| project Timestamp, DeviceName, AccountName, ProcessCommandLineDeviceRegistryEvents
| where Timestamp > ago(7d)
| where RegistryKey has @"Windows Defender\Exclusions"
| project Timestamp, DeviceName, AccountName, RegistryKey, RegistryValueName, RegistryValueDataDeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "-EncodedCommand")
| where InitiatingProcessFileName !in~ ("svchost.exe", "services.exe")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileNameDiscovery 2
Discovery is the reconnaissance an attacker runs after landing on a host, mapping the domain, the network, and the privileges available to them. Any single discovery command is benign (administrators run them too), so these queries lean on volume and clustering: a burst of recon commands from one process in a short window is the pattern, not the individual command. This is an early-stage signal, useful for catching an intrusion before the attacker acts on what they found.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("whoami.exe", "hostname.exe", "ipconfig.exe", "net.exe", "systeminfo.exe", "tasklist.exe")
| summarize CommandCount = dcount(FileName), Commands = make_set(FileName) by DeviceName, AccountName, bin(Timestamp, 10m)
| where CommandCount >= 4DeviceProcessEvents
| where Timestamp > ago(7d)
| where ProcessCommandLine has_any ("nltest /dclist", "net group \"domain admins\"", "dsquery", "Get-ADUser", "Get-ADComputer")
| where AccountName !endswith "$"
| summarize Commands = make_set(ProcessCommandLine), Count = count() by DeviceName, AccountName, bin(Timestamp, 1h)
| where Count > 3Collection 2
Collection is the attacker gathering what they came for before taking it, so it sits right before exfiltration in the kill chain and is a strong point to intervene. On the endpoint, the signal is archive creation, data being staged into a zip or rar for transfer. In the cloud, the highest-value collection signal for business email compromise is inbox rule creation: an attacker who has taken a mailbox sets a rule to hide or forward mail.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("7z.exe", "7za.exe", "rar.exe", "winrar.exe") or (FileName =~ "powershell.exe" and ProcessCommandLine has "Compress-Archive")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLineOfficeActivity
| where TimeGenerated > ago(7d)
| where Operation in ("New-InboxRule", "Set-InboxRule")
| where Parameters has_any ("DeleteMessage", "ForwardTo", "RedirectTo", "MoveToFolder")
| project TimeGenerated, UserId, Operation, Parameters, ClientIPExfiltration 1
Exfiltration is data leaving your environment, the outcome most incidents are ultimately about. It is also the hardest to detect cleanly from volume alone, which is why the single query here is marked low-confidence: large outbound transfers happen constantly through legitimate cloud sync, backups, and software updates. The value is in correlation, a large transfer becomes meaningful when it follows collection and credential access on the same host, or goes to a destination nothing else talks to.
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "ConnectionSuccess"
| where RemotePort in (80, 443, 8080, 8443)
| where RemoteIPType == "Public"
| summarize TotalBytesSent = sum(SentBytes) by DeviceName, RemoteIP
| where TotalBytesSent > 50000000
| order by TotalBytesSent descCommand and Control 1
Command and control is the channel an attacker uses to operate a foothold remotely, and catching it means catching an active, ongoing compromise. Beaconing, the implant calling home at regular intervals, is the canonical signal, and the query here looks for that periodicity in network connections rather than any single connection. Modern C2 hides in normal-looking HTTPS, so the timing pattern is often a better discriminator than the destination.
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "ConnectionSuccess"
| where RemoteIPType == "Public"
| summarize ConnectionTimes = make_list(Timestamp), Count = count() by DeviceName, RemoteIP, RemotePort
| where Count > 50Monitoring 3
These are not threat detections but health checks: queries that tell you whether your detection coverage is actually working. Stale MDE sensors, out-of-date AV signatures, and ASR audit visibility are the blind spots attackers exploit, and a gap here silently degrades every other query on this page. Run these on a schedule and treat a coverage gap with the same urgency as a detection, because an unmonitored host is where the next intrusion will land.
DeviceInfo
| where Timestamp > ago(30d)
| summarize LastSeen = max(Timestamp) by DeviceId, DeviceName, OSPlatform
| where LastSeen < ago(7d)
| extend DaysSilent = datetime_diff('day', now(), LastSeen)
| order by DaysSilent descDeviceInfo
| where Timestamp > ago(1d)
| summarize arg_max(Timestamp, *) by DeviceId
| extend SignatureAge = datetime_diff('day', now(), AntivirusSignatureUpdateTime)
| where SignatureAge > 3
| project DeviceName, OSPlatform, AntivirusSignatureUpdateTime, SignatureAgeDeviceEvents
| where Timestamp > ago(30d)
| where ActionType startswith "Asr"
| summarize AuditCount = countif(ActionType endswith "Audited"), BlockCount = countif(ActionType endswith "Blocked") by ActionType
| order by AuditCount descFrom queries to detections that hold up
These hunt and detect. Windows Endpoint Investigation teaches the method behind them: validating a hit, ruling out the false positive, and reconstructing what the query surfaced into a defensible account of the incident.
Explore the course