Query Details

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 desc

About this query

Suspicious Non-Interactive Sign-Ins with New IP

Query Information

MITRE ATT&CK Technique(s)

Technique IDTitleLink
T1078Valid Accountshttps://attack.mitre.org/techniques/T1078
T1078.004Cloud Accountshttps://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>

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:

  1. 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.

  2. Identify Sessions with Multiple User Agents: It first identifies sessions that have at least two different user agents, which can indicate unusual activity.

  3. 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.

  4. 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.

  5. 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.

  6. 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 profile picture

Benjamin Zulliger

Released: May 21, 2026

Tables

AADNonInteractiveUserSignInLogsEntraIdSignInEvents

Keywords

MicrosoftEntraIDUserUserAgentIPAddressAppDisplayNameCorrelationIdSessionTimestampReportIdAccountUpn

Operators

letagodynamicwhereisnotemptysummarizedcountprojectjoinkindhint.strategyextendcasehashas_anystartswithreplace_regexmake_setarray_lengthandnotdatetime_diffset_differencedistinctmv-expandtotypeofoncoalescetodoublecountifminmaxanybynowsortasc

MITRE Techniques

Actions

GitHub