KQL Threat Hunting in the Real World
Dashboards help. Alerts help. But real investigations usually begin one level deeper.
At GEMXIT, KQL is how we validate suspicious activity, hunt weak signals, pivot across identity, endpoint, email and network telemetry, and work out what really happened.
This is where security operations becomes practical. Good hunting is not about writing fancy queries for the sake of it. It is about asking the right question, following the evidence, and building detections that reflect what attackers actually do.
Briefing summary
KQL is where weak signals become visible. GEMXIT uses it to investigate suspicious PowerShell, risky sign-ins, spoofed email, persistence, reconnaissance and unusual outbound activity across Microsoft security tooling.
What good KQL hunting actually looks like
A practical starting hunt
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
let SuspiciousPowerShell = DeviceProcessEvents | where Timestamp > ago(7d) | where FileName in~ ("powershell.exe", "pwsh.exe") | where ProcessCommandLine has_any ("-enc", "-encodedcommand", "FromBase64String", "-w hidden", "bypass", "downloadstring", "iex") | project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName; let RiskySignins = SigninLogs | where TimeGenerated > ago(7d) | where ResultType == 0 | where RiskLevelDuringSignIn !in ("none", "hidden") or RiskEventTypes_V2 has_any ("unlikelyTravel", "anonymizedIPAddress", "maliciousIPAddress", "unfamiliarFeatures") | project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, RiskLevelDuringSignIn, RiskEventTypes_V2; let SpoofedEmailSignals = EmailEvents | where Timestamp > ago(7d) | where AuthenticationDetails has_any ("spf=fail", "dkim=fail", "dmarc=fail") | project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, AuthenticationDetails, ThreatTypes; SuspiciousPowerShell | order by Timestamp desc
Real KQL examples we actually care about
Example: PowerShell launched by Office or browser activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
DeviceProcessEvents | where Timestamp > ago(7d) | where FileName in~ ("powershell.exe", "pwsh.exe") | where InitiatingProcessFileName in~ ("winword.exe", "excel.exe", "outlook.exe", "chrome.exe", "msedge.exe") | project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine, InitiatingProcessCommandLine | order by Timestamp desc
Example: failed sign-ins followed by success
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let Failed = SigninLogs | where TimeGenerated > ago(24h) | where ResultType != 0 | summarize FailedCount = count() by UserPrincipalName, IPAddress, bin(TimeGenerated, 15m); let Success = SigninLogs | where TimeGenerated > ago(24h) | where ResultType == 0 | summarize SuccessCount = count() by UserPrincipalName, IPAddress, bin(TimeGenerated, 15m); Failed | join kind=inner Success on UserPrincipalName, IPAddress, TimeGenerated | where FailedCount >= 5
Example: persistence and suspicious outbound activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
DeviceRegistryEvents | where Timestamp > ago(7d) | where RegistryKey has_any ( @"\Software\Microsoft\Windows\CurrentVersion\Run", @"\Software\Microsoft\Windows\CurrentVersion\RunOnce" ) | project Timestamp, DeviceName, AccountName, RegistryKey, RegistryValueName, RegistryValueData, InitiatingProcessFileName; DeviceNetworkEvents | where Timestamp > ago(7d) | where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "mshta.exe", "rundll32.exe") | project Timestamp, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteIP, RemotePort, RemoteUrl
How GEMXIT uses KQL day to day
Final thought
If you want help improving Microsoft security visibility, threat hunting and detection quality, 👉 review Microsoft security operations
Develop IT. Protect IT. GEMXIT PTY LTD | GEMXIT UK LTD