Microsoft Sentinel KQL Reference
Overview
KQL (Kusto Query Language) is the query language for Microsoft Sentinel, Azure Monitor, and Azure Data Explorer.
Basic Syntax
TableName
| where Condition
| summarize Count=count() by Field
| order by Count desc
| take 100
KQL uses pipes (|) to chain operators. Each operator transforms the data from the previous step.
|
Common Tables
| Table | Description | Key Fields |
|---|---|---|
|
Windows Security Events |
|
|
Linux syslog |
|
|
Azure AD sign-ins |
|
|
Azure AD audit |
|
|
CEF format logs |
|
|
Azure subscription activity |
|
|
Defender for Endpoint |
|
|
Microsoft 365 activity |
|
|
Threat intelligence |
|
|
Security alerts |
|
Time Filters
Time Functions
| Function | Description | Example |
|---|---|---|
|
Relative time from now |
|
|
Current time |
|
|
Specific datetime |
|
|
Time range |
|
|
Start of day |
|
|
End of day |
|
Operators
Filtering (where)
// Equality
| where EventID == 4625
// Not equal
| where EventID != 4624
// Contains (case-insensitive)
| where Account contains "admin"
// Contains (case-sensitive)
| where Account contains_cs "Admin"
// Starts with
| where Account startswith "svc_"
// Ends with
| where Account endswith "_admin"
// Regex match
| where Account matches regex @"^admin\d+"
// In set
| where EventID in (4624, 4625, 4648)
// Not in set
| where EventID !in (4624, 4625)
// Between (inclusive)
| where EventID between (4624 .. 4648)
// Null check
| where isnotempty(Account)
| where isempty(Account)
Aggregation (summarize)
// Count
| summarize count() by Account
// Count distinct
| summarize dcount(Account) by Computer
// Sum
| summarize TotalBytes = sum(BytesReceived)
// Average
| summarize AvgDuration = avg(Duration)
// Min/Max
| summarize MinTime = min(TimeGenerated), MaxTime = max(TimeGenerated)
// Multiple aggregations
| summarize
Count = count(),
UniqueUsers = dcount(Account),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Computer
// Time binning
| summarize count() by bin(TimeGenerated, 1h)
Projection (project)
// Select specific columns
| project TimeGenerated, Account, Computer, EventID
// Rename columns
| project Time = TimeGenerated, User = Account
// Computed columns
| project
Account,
EventID,
IsSuccess = EventID == 4624
// Remove columns
| project-away RawData, MG
// Reorder columns
| project-reorder TimeGenerated, Account, Computer
Sorting and Limiting
// Sort descending
| order by TimeGenerated desc
// Sort ascending
| sort by Count asc
// Take first N rows
| take 100
// Top N by value
| top 10 by Count desc
Joining Data
// Inner join
SecurityEvent
| where EventID == 4624
| join kind=inner (
SecurityEvent
| where EventID == 4625
) on Account
// Left outer join
let FailedLogins = SecurityEvent | where EventID == 4625;
SecurityEvent
| where EventID == 4624
| join kind=leftouter FailedLogins on Account
// Anti-join (rows in left not in right)
let Admins = datatable(Account:string) ["admin", "administrator"];
SecurityEvent
| join kind=leftanti Admins on Account
String Functions
// Extract with regex
| extend Domain = extract(@"([^\\]+)\\", 1, Account)
// Split string
| extend Parts = split(Account, "\\")
| extend Domain = Parts[0], User = Parts[1]
// Concatenate
| extend FullName = strcat(FirstName, " ", LastName)
// Replace
| extend CleanAccount = replace_string(Account, "@domain.com", "")
// Substring
| extend ShortName = substring(Account, 0, 10)
// Case conversion
| extend LowerAccount = tolower(Account)
| extend UpperAccount = toupper(Account)
// Trim
| extend Trimmed = trim(@"\s", Account)
Parsing
// Parse structured data
| parse EventData with * "TargetUserName\">" TargetUser "</Data>" *
// Parse with regex
| extend IP = extract(@"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", 1, Message)
// Parse JSON
| extend ParsedData = parse_json(ExtendedProperties)
| extend AlertType = ParsedData.alertType
// Parse URL
| extend UrlParts = parse_url(RequestUrl)
| extend Host = UrlParts.Host, Path = UrlParts.Path
Authentication Queries
Failed Sign-in Analysis
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != 0 // Failed
| summarize
FailureCount = count(),
DistinctIPs = dcount(IPAddress),
DistinctApps = dcount(AppDisplayName)
by UserPrincipalName, ResultDescription
| order by FailureCount desc
| take 25
Brute Force Detection
// 10+ failures in 10 minutes
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType != 0
| summarize
FailureCount = count(),
IPs = make_set(IPAddress),
FirstAttempt = min(TimeGenerated),
LastAttempt = max(TimeGenerated)
by UserPrincipalName, bin(TimeGenerated, 10m)
| where FailureCount >= 10
| order by FailureCount desc
Successful After Failed
let FailedLogins = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != 0
| summarize FailCount = count() by UserPrincipalName;
let SuccessfulLogins = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| summarize SuccessCount = count() by UserPrincipalName;
FailedLogins
| join kind=inner SuccessfulLogins on UserPrincipalName
| where FailCount >= 5
| project UserPrincipalName, FailCount, SuccessCount
| order by FailCount desc
Impossible Travel
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, Location, IPAddress
| order by UserPrincipalName, TimeGenerated asc
| serialize
| extend PrevLocation = prev(Location, 1), PrevTime = prev(TimeGenerated, 1), PrevUser = prev(UserPrincipalName, 1)
| where UserPrincipalName == PrevUser
| where Location != PrevLocation
| extend TimeDiff = datetime_diff('minute', TimeGenerated, PrevTime)
| where TimeDiff < 60 // Less than 1 hour between different locations
| project TimeGenerated, UserPrincipalName, Location, PrevLocation, TimeDiff, IPAddress
Off-Hours Authentication
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| extend Hour = datetime_part("hour", TimeGenerated)
| extend DayOfWeek = dayofweek(TimeGenerated)
| where Hour < 6 or Hour >= 20 or DayOfWeek in (6d, 7d) // Off-hours or weekend
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, Location
| order by TimeGenerated desc
MFA Status Analysis
SigninLogs
| where TimeGenerated > ago(24h)
| summarize
TotalSignins = count(),
MFARequired = countif(AuthenticationRequirement == "multiFactorAuthentication"),
SingleFactor = countif(AuthenticationRequirement == "singleFactorAuthentication")
by UserPrincipalName
| extend MFAPercentage = round(100.0 * MFARequired / TotalSignins, 1)
| order by SingleFactor desc
Windows Security Events
Event ID Reference
| Event ID | Description |
|---|---|
4624 |
Successful logon |
4625 |
Failed logon |
4634 |
Logoff |
4648 |
Explicit credentials logon |
4672 |
Special privileges assigned |
4688 |
New process created |
4720 |
User account created |
4722 |
User account enabled |
4725 |
User account disabled |
4726 |
User account deleted |
4732 |
Member added to local group |
4756 |
Member added to universal group |
4768 |
Kerberos TGT requested |
4769 |
Kerberos service ticket requested |
4776 |
NTLM authentication |
5140 |
Network share accessed |
5145 |
Network share object accessed |
7045 |
Service installed |
Failed Logon Reasons
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4625
| summarize Count = count() by Status, SubStatus
| extend Reason = case(
SubStatus == "0xc0000064", "User does not exist",
SubStatus == "0xc000006a", "Wrong password",
SubStatus == "0xc0000234", "Account locked",
SubStatus == "0xc0000072", "Account disabled",
SubStatus == "0xc0000193", "Account expired",
SubStatus == "0xc0000070", "Workstation restriction",
SubStatus == "0xc000006f", "Time restriction",
SubStatus == "0xc0000224", "Password must change",
"Unknown"
)
| project Reason, Count
| order by Count desc
Process Creation Analysis
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4688
| where NewProcessName has_any ("powershell", "cmd", "wscript", "cscript", "mshta", "regsvr32")
| project
TimeGenerated,
Computer,
Account,
NewProcessName,
CommandLine
| order by TimeGenerated desc
| take 100
Suspicious PowerShell
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4688
| where NewProcessName endswith "powershell.exe"
| where CommandLine has_any (
"-encodedcommand", "-enc ", "-e ",
"downloadstring", "downloadfile",
"invoke-expression", "iex ",
"bypass", "-noprofile", "-windowstyle hidden",
"frombase64string"
)
| project TimeGenerated, Computer, Account, CommandLine
| order by TimeGenerated desc
New Service Installation
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 7045
| project TimeGenerated, Computer, ServiceName = tostring(EventData.ServiceName),
ServiceFileName = tostring(EventData.ImagePath),
ServiceAccount = tostring(EventData.AccountName)
| order by TimeGenerated desc
Network Analysis
CommonSecurityLog Analysis
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Palo Alto Networks" or DeviceVendor == "Fortinet"
| where Activity == "TRAFFIC"
| summarize
Connections = count(),
TotalBytes = sum(SentBytes + ReceivedBytes)
by SourceIP, DestinationIP, DestinationPort
| top 25 by TotalBytes desc
DNS Analysis
DnsEvents
| where TimeGenerated > ago(24h)
| summarize QueryCount = count() by Name, ClientIP
| where QueryCount > 100
| order by QueryCount desc
| take 50
Outbound Traffic to Rare Destinations
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where Activity == "TRAFFIC" and DeviceAction == "allow"
| summarize
TotalConnections = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by DestinationIP
| where TotalConnections < 5 // Rare destinations
| order by LastSeen desc
Threat Hunting
Known Malicious IPs
let MaliciousIPs = ThreatIntelligenceIndicator
| where TimeGenerated > ago(30d)
| where NetworkIP != ""
| distinct NetworkIP;
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DestinationIP in (MaliciousIPs) or SourceIP in (MaliciousIPs)
| project TimeGenerated, SourceIP, DestinationIP, DeviceVendor, Activity
Lateral Movement Detection
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID in (4624, 4648)
| where LogonType in (3, 10) // Network and RemoteInteractive
| summarize
TargetCount = dcount(Computer),
Targets = make_set(Computer)
by Account
| where TargetCount > 5
| order by TargetCount desc
Pass-the-Hash/Pass-the-Ticket
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4624
| where LogonType == 9 // NewCredentials
| where AuthenticationPackageName == "Negotiate"
| project TimeGenerated, Computer, Account, IpAddress, LogonProcessName
| order by TimeGenerated desc
Kerberoasting Detection
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4769
| where ServiceName !endswith "$"
| where TicketEncryptionType has_any ("0x17", "0x18") // RC4
| summarize RequestCount = count() by Account, ServiceName
| where RequestCount > 5
| order by RequestCount desc
Data Exfiltration Indicators
// Large uploads
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where Activity == "TRAFFIC"
| where SentBytes > 100000000 // 100 MB
| summarize
TotalSent = sum(SentBytes),
Connections = count()
by SourceIP, DestinationIP
| extend TotalSentMB = round(TotalSent / 1048576.0, 2)
| order by TotalSent desc
| take 25
Alert Management
Recent Alerts by Severity
SecurityAlert
| where TimeGenerated > ago(24h)
| summarize Count = count() by AlertSeverity, AlertName
| order by AlertSeverity asc, Count desc
Useful Patterns
Dynamic Thresholds
// Compare current to baseline
let Baseline = SigninLogs
| where TimeGenerated between (ago(30d) .. ago(7d))
| summarize BaselineCount = count() by UserPrincipalName;
let Current = SigninLogs
| where TimeGenerated > ago(24h)
| summarize CurrentCount = count() by UserPrincipalName;
Current
| join kind=inner Baseline on UserPrincipalName
| extend Ratio = round(CurrentCount * 1.0 / BaselineCount, 2)
| where Ratio > 3 // 3x increase
| order by Ratio desc
Performance Tips
-
Filter early - Use
wherebeforesummarize -
Use
hasinstead ofcontains-hasis faster for whole-word matching -
Limit time range - Always specify
TimeGenerated > ago() -
Use
take- Limit results during exploration -
Project columns - Select only needed columns
-
Avoid
*in join - Specify columns explicitly