Suspicious Non Interactive Sign Ins With New IP
Query
let lookback = 1h;
let historyStart = ago(29d);
let historyEnd = ago(lookback);
let ExcludedApps = dynamic([
"Windows Sign In",
"Viva Engage"
]);
// Vorfilter: nur CorrelationIds mit mind. 2 verschiedenen UserAgents
let MultiUASessions =
AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(lookback)
| where ResultType == 0
| where isnotempty(UserAgent)
| where isnotempty(CorrelationId)
| summarize UACount_pre = dcount(UserAgent) by CorrelationId
| where UACount_pre >= 2
| project CorrelationId;
let SuspiciousSessions =
AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(lookback)
| where ResultType == 0
| where isnotempty(UserAgent)
| where isnotempty(CorrelationId)
// Nur Sessions mit mind. 2 UAs weiterverarbeiten
| join kind=inner hint.strategy=broadcast (MultiUASessions) on CorrelationId
| extend UAType = case(
UserAgent has "MSAL", "MSAL-Client",
UserAgent has "Windows-AzureAD-Authentication-Provider", "Windows-Auth-Provider",
UserAgent has "Dalvik", "Android-App",
UserAgent has "CFNetwork", "iOS-App",
UserAgent has "Microsoft Authenticator", "Authenticator-App",
UserAgent has_any ("python", "curl", "powershell",
"okhttp", "axios", "go-http",
"java/", "requests", "wget"), "Suspicious-Tool",
UserAgent startswith "Mozilla", "Browser",
"Unknown"
)
| extend UANormalized = replace_regex(UserAgent, @'\d+\.\d+\.\d+\.\d+', "x.x.x.x")
| extend UANormalized = replace_regex(UANormalized, @';\s*WebView/[\d\.]+', "")
| summarize
UATypes = make_set(UAType, 6),
UAList = make_set(UANormalized, 6),
IPList = make_set(IPAddress, 10),
AppList = make_set(AppDisplayName, 5),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
SignInCount = count()
by UserPrincipalName, CorrelationId
| extend UACount = array_length(UAList)
| extend IPCount = array_length(IPList)
| extend HasSuspiciousTool = UATypes has "Suspicious-Tool"
| extend HasBrowser = UATypes has "Browser"
| extend IsUASuspicious = case(
HasSuspiciousTool == true, true,
HasBrowser and UACount >= 2, true,
false
)
| extend IsMixedLegitimate = (
UATypes has "MSAL-Client" or
UATypes has "Android-App" or
UATypes has "iOS-App" or
UATypes has "Authenticator-App"
) and not(HasSuspiciousTool) and not(HasBrowser)
| extend SessionDurationMin = datetime_diff('minute', LastSeen, FirstSeen)
| extend AllAppsExcluded = array_length(set_difference(AppList, ExcludedApps)) == 0
| where IsUASuspicious == true
| where not(IsMixedLegitimate)
| where SessionDurationMin <= 60
| where not(AllAppsExcluded)
| where not(
UATypes has "Windows-Auth-Provider"
and UATypes has "Browser"
and UACount == 2
and AppList has "Windows Sign In"
)
| where not(
UATypes has "Windows-Auth-Provider"
and UATypes has "Authenticator-App"
and not(HasSuspiciousTool)
);
let SuspiciousUsers = SuspiciousSessions
| distinct UserPrincipalName;
let HistoricalIPs =
EntraIdSignInEvents
| where Timestamp >= historyStart and Timestamp < historyEnd
| where ErrorCode == 0
| summarize hint.shufflekey=AccountUpn
IPSeenCount = count(),
HistoricalIPs = make_set(IPAddress, 200),
ReportId = max(ReportId)
by AccountUpn, IPAddress
| where AccountUpn in (SuspiciousUsers);
SuspiciousSessions
| mv-expand CurrentIP = IPList to typeof(string)
| join hint.strategy=broadcast kind=leftouter (HistoricalIPs)
on $left.UserPrincipalName == $right.AccountUpn
and $left.CurrentIP == $right.IPAddress
| extend IPSeenBefore = isnotempty(IPSeenCount)
| extend IPSeenCount = coalesce(todouble(IPSeenCount), 0.0)
| summarize
UATypes = any(UATypes),
UACount = any(UACount),
UAList = any(UAList),
IPList = any(IPList),
IPCount = any(IPCount),
AppList = any(AppList),
FirstSeen = any(FirstSeen),
LastSeen = any(LastSeen),
SignInCount = any(SignInCount),
HasSuspiciousTool = any(HasSuspiciousTool),
HasBrowser = any(HasBrowser),
IsUASuspicious = any(IsUASuspicious),
SessionDurationMin = any(SessionDurationMin),
ReportId = any(ReportId),
NewIPCount = countif(IPSeenBefore == false),
MinIPSeenCount = min(IPSeenCount),
MaxIPSeenCount = max(IPSeenCount)
by UserPrincipalName, CorrelationId
| extend WorstIPRisk = case(
NewIPCount > 0, "High - new IP for this User",
MinIPSeenCount < 3, "Medium - rare IP",
"Low - known IP"
)
| extend Severity = case(
HasSuspiciousTool and NewIPCount > 0, "High",
IsUASuspicious and WorstIPRisk startswith "High", "High",
IsUASuspicious and WorstIPRisk startswith "Medium", "Medium",
"Low"
)
| where Severity in ("High", "Medium")
| where MaxIPSeenCount < 2
| extend TimeGenerated = now()
| project
TimeGenerated,
Severity,
UserPrincipalName,
CorrelationId,
ReportId,
UACount,
UATypes,
UAList,
HasSuspiciousTool,
SessionDurationMin,
NewIPCount,
IPCount,
IPList,
MinIPSeenCount,
MaxIPSeenCount,
WorstIPRisk,
AppList,
FirstSeen,
LastSeen,
SignInCount
| sort by Severity asc, NewIPCount descAbout this query
Suspicious Non-Interactive Sign-Ins with New IP
Query Information
MITRE ATT&CK Technique(s)
| Technique ID | Title | Link |
|---|---|---|
| T1078 | Valid Accounts | https://attack.mitre.org/techniques/T1078 |
| T1078.004 | Cloud Accounts | https://attack.mitre.org/techniques/T1078/004 |
Description
This detection query identifies anomalous non-interactive Microsoft Entra ID sign-ins by analyzing automated session data and cross-referencing it with historical user baselines. It targets non-interactive sign-ins that utilize suspicious tools (such as Python, PowerShell, or curl) or exhibit unusual browser behavior within the same correlation ID. The query then evaluates these sessions against a 30-day historical IP profile for each user, prioritizing alerts where a suspicious user-agent signature coincides with an entirely new or rare IP address.
Author <Optional>
- Name: Benjamin Zulliger
- Github: https://github.com/benscha/KQLAdvancedHunting
- LinkedIn: https://www.linkedin.com/in/benjamin-zulliger/
Defender XDR
Explanation
This query is designed to detect suspicious non-interactive sign-ins to Microsoft Entra ID (formerly Azure Active Directory) by analyzing user sign-in patterns and identifying anomalies. Here's a simplified breakdown of what the query does:
-
Time Frame and Exclusions: The query looks at sign-in data from the past hour and excludes certain applications like "Windows Sign In" and "Viva Engage" from consideration.
-
Identify Sessions with Multiple User Agents: It first identifies sessions that have at least two different user agents, which can indicate unusual activity.
-
Detect Suspicious Sessions: It focuses on sessions that use potentially suspicious tools (like Python, PowerShell, or curl) or exhibit unusual browser behavior. These sessions are flagged if they involve new or rare IP addresses for the user.
-
Historical IP Analysis: The query checks the IP addresses used in these sessions against a 30-day history of IP addresses for each user to determine if the IP is new or rarely used.
-
Risk and Severity Assessment: Each session is assessed for risk based on the novelty of the IP address and the presence of suspicious tools. Sessions are categorized into "High", "Medium", or "Low" severity based on these factors.
-
Filter and Output: Only sessions with "High" or "Medium" severity are included in the final output. The results are sorted by severity and the number of new IPs, providing a prioritized list of potentially suspicious sign-in activities.
Overall, this query helps security teams identify and prioritize non-interactive sign-ins that may indicate compromised accounts or unauthorized access attempts, focusing on new or rare IP addresses and suspicious user-agent patterns.
Details

Benjamin Zulliger
Released: May 21, 2026
Tables
Keywords
Operators