Most enterprise environments are hybrid now — on-premises Active Directory synced to Entra ID, with the same credentials used to access cloud resources. Attackers know this and exploit it actively. A compromised on-prem account becomes a compromised cloud identity. A stolen Entra ID token can be replayed from anywhere in the world without triggering most endpoint detections.
Entra ID sign-in logs in Microsoft Sentinel are one of the richest detection data sources available — but most environments are either not ingesting them at the right verbosity or not building detections that actually catch the techniques being used. Here are the queries that work in production.
All queries below require Entra ID Sign-in Logs and Audit Logs ingested into Microsoft Sentinel via the Microsoft Entra ID connector. Ensure both SigninLogs and AuditLogs tables are being populated before building these detections. Also verify that non-interactive sign-in logs are enabled — many token theft patterns only appear in non-interactive logs.
1. MFA Bypass — Adversary-in-the-Middle (AiTM)
AiTM phishing proxies sit between the user and the legitimate login page, relaying authentication in real time and capturing the post-MFA session token. From Entra ID's perspective, the authentication looks completely legitimate — MFA was satisfied, the user successfully logged in. The signal is in what happens after the session is established: the token gets replayed from a different IP or location than where the original authentication occurred.
let AuthWindow = 1h; SigninLogs | where TimeGenerated > ago(1d) | where ResultType == "0" | where AuthenticationRequirement == "multiFactorAuthentication" | summarize Locations = make_set(Location), IPs = make_set(IPAddress), UserAgents = make_set(UserAgent), AuthCount = count() by UserPrincipalName, bin(TimeGenerated, AuthWindow) // Token replay: same user, multiple IPs or locations // in the same window after successful MFA | where array_length(IPs) > 1 or array_length(Locations) > 1 | order by AuthCount desc
2. Impossible Travel
Two successful sign-ins from the same account in locations that are physically impossible to travel between in the time window between them. The challenge is building this correctly — many VPN configurations and cloud provider IP ranges generate false positives if you don't filter them out.
let ImpossibleSpeedKmH = 800; SigninLogs | where TimeGenerated > ago(1d) | where ResultType == "0" | where isnotempty(LocationDetails) | extend Lat = toreal(LocationDetails.geoCoordinates.latitude), Lon = toreal(LocationDetails.geoCoordinates.longitude) | where isnotempty(Lat) | sort by UserPrincipalName, TimeGenerated asc | serialize | extend PrevLat = prev(Lat), PrevLon = prev(Lon), PrevTime = prev(TimeGenerated), PrevUser = prev(UserPrincipalName) | where UserPrincipalName == PrevUser | extend TimeDiffHours = datetime_diff('minute', TimeGenerated, PrevTime) / 60.0 | extend DistanceKm = geo_distance_2points( Lon, Lat, PrevLon, PrevLat) / 1000 | where TimeDiffHours > 0 | extend SpeedKmH = DistanceKm / TimeDiffHours | where SpeedKmH > ImpossibleSpeedKmH | project TimeGenerated, UserPrincipalName, Location, IPAddress, SpeedKmH, DistanceKm | order by SpeedKmH desc
3. Entra ID Privileged Role Changes
Any change to Global Administrator, Privileged Role Administrator, or other highly privileged roles in Entra ID should alert immediately. Attackers who compromise a privileged account often add a second account to a high-privilege role as a persistence mechanism — and this is exactly the event that gets missed when teams are focused on endpoint detections.
AuditLogs | where TimeGenerated > ago(7d) | where OperationName in( "Add member to role", "Add eligible member to role", "Add scoped member to role" ) | extend RoleName = tostring( TargetResources[0].displayName) | where RoleName in~( "Global Administrator", "Privileged Role Administrator", "Security Administrator", "Exchange Administrator", "SharePoint Administrator", "Conditional Access Administrator" ) | extend InitiatedByUser = tostring( InitiatedBy.user.userPrincipalName), TargetUser = tostring( TargetResources[1].userPrincipalName) | project TimeGenerated, OperationName, RoleName, InitiatedByUser, TargetUser, Result, CorrelationId | order by TimeGenerated desc
Any Global Administrator role assignment outside of a known provisioning process is a P1. Treat it the same way you'd treat a Domain Admins change in on-premises AD — contain first, investigate after.
4. Suspicious OAuth App Consent
Consent phishing is increasingly common — attackers trick users into granting an OAuth app access to their mailbox, files, or contacts. The app looks legitimate (often impersonating Microsoft or a known SaaS product) and once consent is granted, the attacker has persistent access that survives password resets and MFA changes.
AuditLogs | where TimeGenerated > ago(7d) | where OperationName == "Consent to application" | extend AppName = tostring( TargetResources[0].displayName), ConsentedBy = tostring( InitiatedBy.user.userPrincipalName), Permissions = tostring( AdditionalDetails) | // Flag high-risk permission scopes | where Permissions has_any( "Mail.ReadWrite", "Mail.Send", "Files.ReadWrite.All", "Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory" ) | project TimeGenerated, AppName, ConsentedBy, Permissions, Result | order by TimeGenerated desc
5. Guest Account Abuse
Guest accounts in Entra ID are a common blind spot. External users invited to a tenant often have more access than intended, and compromised guest accounts from partner or vendor organizations are an increasingly common initial access vector. The signal is guest account activity that looks like internal user behavior.
SigninLogs | where TimeGenerated > ago(7d) | where UserType == "Guest" | where ResultType == "0" | summarize SigninCount = count(), AppAccessed = make_set(AppDisplayName), Locations = make_set(Location), IPs = make_set(IPAddress) by UserPrincipalName, bin(TimeGenerated, 1d) // Flag guests accessing many apps or from many locations | where array_length(AppAccessed) > 5 or array_length(Locations) > 3 | order by SigninCount desc
6. Risky Sign-in Baseline
Entra ID Protection generates risk signals natively — but most environments have it configured to alert rather than block, and those alerts often sit in a queue nobody is watching. This query pulls high-risk sign-ins that succeeded despite the risk signal, which is the combination that matters most.
SigninLogs | where TimeGenerated > ago(1d) | where RiskLevelDuringSignIn in( "high", "medium") | where ResultType == "0" | project TimeGenerated, UserPrincipalName, RiskLevelDuringSignIn, RiskDetail, Location, IPAddress, AppDisplayName, AuthenticationRequirement | order by RiskLevelDuringSignIn, TimeGenerated desc
Entra ID and cloud identity detection is increasingly part of the weekly rotation — alongside on-premises Sentinel, CrowdStrike, and Splunk rules. Every issue includes the full detection with investigation steps, false positive guidance, and MITRE mapping. Platforms rotate so you get broad coverage over time.
Join — $14.99/mo See a Sample Issue