Query Details
// In case you have not blocked yet Device Code
let query_frequency = 5m;
let query_period = 2d;
let query_wait = 10m;
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated between (ago(query_frequency + query_wait) .. ago(query_wait))
| where AuthenticationProtocol == "deviceCode" and ResultType == 0
| join kind=rightsemi (
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs, ADFSSignInLogs
| where TimeGenerated > ago(query_period + query_wait)
) on CorrelationId, UserId
| summarize
IPAddresses = make_set_if(IPAddress, not(Type == "ADFSSignInLogs")),
ADFSIPAddresses = make_set_if(IPAddress, Type == "ADFSSignInLogs" and isnotempty(parse_ipv4(IPAddress)) and not(ipv4_is_private(IPAddress))),
Locations = make_set_if(Location, isnotempty(Location)),
AutonomousSystemNumbers = make_set_if(AutonomousSystemNumber, isnotempty(AutonomousSystemNumber)),
UserAgents = make_set_if(UserAgent, isnotempty(UserAgent)),
SessionIds = make_set_if(SessionId, isnotempty(SessionId)),
OriginalRequestId = take_anyif(OriginalRequestId, AuthenticationProtocol == "deviceCode" and ResultType == 0)
by CorrelationId, UserId
| extend IPAddresses = set_union(IPAddresses, ADFSIPAddresses)
| as _Auxiliar
| mv-expand SessionId = iff(array_length(SessionIds) > 0, SessionIds, dynamic([""])) to typeof(string)
| join kind=leftouter (
union SigninLogs, AADNonInteractiveUserSignInLogs// ADFSSignInLogs does not have a SessionId column
| where TimeGenerated between (ago(query_period + query_wait) .. ago(query_frequency + query_wait))
| where (isnotempty(SessionId) and SessionId in (toscalar(_Auxiliar | summarize make_set(SessionIds)))) and not(CorrelationId in (toscalar(_Auxiliar | summarize make_set(CorrelationId))))
| summarize
SessionPreviousIPAddresses = make_set(IPAddress),
SessionPreviousAutonomousSystemNumbers = make_set_if(AutonomousSystemNumber, isnotempty(AutonomousSystemNumber)),
SessionPreviousUserAgents = array_sort_asc(make_set_if(UserAgent, isnotempty(UserAgent)))
by UserId, SessionId
) on UserId, SessionId
| project-away UserId1, SessionId1
| extend
NewIPAddresses = set_difference(IPAddresses, SessionPreviousIPAddresses),
NewAutonomousSystemNumbers = set_difference(AutonomousSystemNumbers, SessionPreviousAutonomousSystemNumbers),
NewUserAgents = array_sort_asc(set_difference(UserAgents, SessionPreviousUserAgents))
| join kind=leftouter (
SigninLogs
| where TimeGenerated > ago(query_period + query_wait)
| where OriginalRequestId in (toscalar(_Auxiliar | summarize make_set(OriginalRequestId)))
| summarize arg_max(TimeGenerated, *) by OriginalRequestId, UserId
) on UserId, OriginalRequestId
| project-away UserId1, OriginalRequestId1, CorrelationId1, SessionId1
| where case(
array_length(Locations) > 1, true,
array_length(NewAutonomousSystemNumbers) > 0 and array_length(SessionPreviousAutonomousSystemNumbers) > 0 , true,
array_length(NewIPAddresses) > 0 and array_length(SessionPreviousIPAddresses) > 0 , true,
//array_length(NewUserAgents) > 0 and array_length(SessionPreviousUserAgents) > 0 , true,
// Maybe you could check when the Location or the AutonomousSystemNumber is unexpected (if they are not in a Watchlist)
// Maybe you could check if the app/resource is not an expected one, for example if it is not Microsoft Azure CLI/04b07795-8ddb-461a-bbee-02f9e1bf7b46, or if it is Microsoft Office/d3590ed6-52b3-4102-aeff-aad2292ab01c
false
)
| project
TimeGenerated,
CreatedDateTime,
Type,
UserDisplayName,
UserPrincipalName,
UserId,
AlternateSignInName,
SignInIdentifier,
UserType,
Locations,
NewIPAddresses,
SessionPreviousIPAddresses,
NewAutonomousSystemNumbers,
SessionPreviousAutonomousSystemNumbers,
NewUserAgents,
SessionPreviousUserAgents,
SessionIds,
AuthenticationProtocol,
ResultType,
ResultSignature,
ResultDescription,
ClientAppUsed,
AppDisplayName,
ResourceDisplayName,
DeviceDetail,
Status,
MfaDetail,
AuthenticationContextClassReferences,
AuthenticationDetails,
AuthenticationProcessingDetails,
AuthenticationRequirement,
AuthenticationRequirementPolicies,
SessionLifetimePolicies,
TokenIssuerType,
IncomingTokenType,
TokenProtectionStatusDetails,
ConditionalAccessStatus,
ConditionalAccessPolicies,
RiskDetail,
RiskEventTypes_V2,
RiskLevelAggregated,
RiskLevelDuringSignIn,
RiskState,
HomeTenantId,
ResourceTenantId,
CrossTenantAccessType,
AppId,
ResourceIdentity,
UniqueTokenIdentifier,
OriginalRequestId,
CorrelationId
This KQL query is designed to detect potentially suspicious sign-in activities related to the "deviceCode" authentication protocol. Here's a simplified breakdown of what the query does:
Time Frame Definition: The query defines three time-related parameters:
query_frequency: 5 minutesquery_period: 2 daysquery_wait: 10 minutesData Collection: It collects sign-in logs from multiple sources (SigninLogs, AADNonInteractiveUserSignInLogs, and ADFSSignInLogs) within specified time frames.
Filter Criteria: It filters the logs to focus on successful sign-ins (ResultType == 0) using the "deviceCode" authentication protocol.
Data Joining: The query joins data from different log sources based on CorrelationId and UserId to correlate related sign-in events.
Data Aggregation: It aggregates various attributes such as IP addresses, locations, user agents, and session IDs for each user and correlation ID.
IP and ASN Analysis: It identifies new IP addresses and Autonomous System Numbers (ASNs) that were not seen in previous sessions for the same user.
Suspicious Activity Detection: The query flags potentially suspicious activities based on:
Result Projection: Finally, it projects a wide range of attributes related to the sign-in events, including user details, authentication details, and risk assessments.
Overall, this query aims to identify unusual sign-in patterns that might indicate unauthorized access or account compromise, focusing on changes in IP addresses, locations, and network identifiers.

Jose Sebastián Canós
Released: April 16, 2026
Tables
Keywords
Operators