Six KQL queries for Entra sign-in logs every admin should keep
Once sign-in logs flow into Log Analytics, a few queries answer most of the questions you'll get. Copy these into a workbook or saved searches.
On this page
- 1. Top sign-in failures
- 2. One user's recent sign-ins
- 3. Sign-ins blocked by Conditional Access
- 4. Successful sign-ins from new countries
- 5. Password spray pattern
- 6. Legacy authentication still in use
- 7. Non-interactive sign-ins by app
- 8. Service principal sign-ins from unexpected IPs
- Tips for writing your own
The Entra portal shows sign-in logs for 30 days with limited filtering. Sending them to a Log Analytics workspace through Diagnostic settings in Entra keeps them for as long as you configure and lets you query them with KQL. Exporting sign-in logs needs Entra ID P1 or P2.
1. Top sign-in failures
SigninLogs
| where TimeGenerated > ago(7d) and ResultType != "0"
| summarize Count = count() by ResultType, ResultDescription
| top 20 by Count2. One user's recent sign-ins
SigninLogs
| where TimeGenerated > ago(3d)
| where UserPrincipalName =~ "jane.doe@contoso.com"
| project TimeGenerated, AppDisplayName, ResultType, ResultDescription,
IPAddress, Location = tostring(LocationDetails.countryOrRegion),
ConditionalAccessStatus
| order by TimeGenerated desc3. Sign-ins blocked by Conditional Access
SigninLogs
| where TimeGenerated > ago(1d) and ConditionalAccessStatus == "failure"
| summarize Count = count() by UserPrincipalName, AppDisplayName
| order by Count desc4. Successful sign-ins from new countries
let known = SigninLogs
| where TimeGenerated between (ago(30d) .. ago(1d)) and ResultType == "0"
| distinct UserPrincipalName, Country = tostring(LocationDetails.countryOrRegion);
SigninLogs
| where TimeGenerated > ago(1d) and ResultType == "0"
| extend Country = tostring(LocationDetails.countryOrRegion)
| join kind=leftanti known on UserPrincipalName, Country
| project TimeGenerated, UserPrincipalName, Country, IPAddress, AppDisplayName5. Password spray pattern
SigninLogs
| where TimeGenerated > ago(1h) and ResultType == "50126"
| summarize Users = dcount(UserPrincipalName) by IPAddress
| where Users > 10
| order by Users desc50126 means invalid username or password. One IP failing against many different users is a classic spray.
6. Legacy authentication still in use
SigninLogs
| where TimeGenerated > ago(14d)
| where ClientAppUsed !in ("Browser", "Mobile Apps and Desktop clients")
| summarize Count = count() by ClientAppUsed, UserPrincipalName, AppDisplayName
| order by Count descAADNonInteractiveUserSignInLogs table. Some investigations need both.7. Non-interactive sign-ins by app
Most sign-in volume is non-interactive: token refreshes by apps the user already signed in to. It's in a separate table and can be large, so only send it if you'll use it.
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d)
| summarize Count = count(), Failures = countif(ResultType != "0") by AppDisplayName
| order by Count desc8. Service principal sign-ins from unexpected IPs
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(7d) and ResultType == "0"
| summarize IPs = make_set(IPAddress, 20), Count = count() by ServicePrincipalName
| where array_length(IPs) > 3
| order by Count descTips for writing your own
- Filter on
TimeGeneratedfirst. It's the cheapest way to make a query fast. - Use
=~for case-insensitive UPN matches. - Dynamic columns like
LocationDetailsandDeviceDetailneedtostring()before you group by them. - Look up any error code with the AADSTS lookup, or paste a single sign-in into the sign-in log explainer.
More ready-made queries are in the KQL library.
This post was last checked against Microsoft's documentation over six months ago. The approach should still hold, but check the linked sources for anything that has changed before you act on it.
You've reached the end of Conditional Access from zeroBack to the path →You've reached the end of Automating Entra and Azure safelyBack to the path →