Query Details

Privileged Account Authentication Method Audit

Query

# *Privileged Account Authentication Method Audit*

## Query Information

#### MITRE ATT&CK Technique(s)

| Technique ID | Title    | Link    |
| ---  | --- | --- |
| T1078.004  | Cloud Accounts | https://attack.mitre.org/techniques/T1078/004/ |


#### Description
This rule audits the authentication posture of highly privileged Entra ID (Azure AD) accounts by analyzing their sign-in activity over the past 30 days. It identifies privileged users and assesses their authentication security by tracking multi-factor authentication (MFA) usage, detecting password-only sign-ins, flagging accounts with limited authentication methods, and identifying inactive privileged accounts. This is intended for risk assessment and identifying potential gaps in MFA enforcement or account hygiene.
 
#### Author <Optional>
- **Name: Benjamin Zulliger**
- **Github: https://github.com/benscha/KQLAdvancedHunting**
- **LinkedIn: https://www.linkedin.com/in/benjamin-zulliger/**

#### References

## Defender XDR
```KQL
let Lookback = 30d;
let PrivilegedRoles = dynamic([
// ===== ENTRA TIER 0 (Critical – Tenant-Control) =====
	"Global Administrator",
	"Privileged Role Administrator",
	"Privileged Authentication Administrator",
	"Security Administrator",
	"Conditional Access Administrator",
	"Application Administrator",
	"Cloud Application Administrator",
	"Hybrid Identity Administrator",
	"Partner Tier2 Support",
// ===== ENTRA TIER 1 (High – Service-Control/ Writepermission) =====
	"Authentication Administrator",
	"Authentication Policy Administrator",
	"Authentication Extensibility Administrator",
	"User Administrator",
	"Helpdesk Administrator",
	"Password Administrator",
	"Directory Writers",
	"Directory Synchronization Accounts",
	"Domain Name Administrator",
	"External Identity Provider Administrator",
	"Lifecycle Workflows Administrator",
	"Groups Administrator",
	"Identity Governance Administrator",
	"Exchange Administrator",
	"SharePoint Administrator",
	"Teams Administrator",
	"Teams Telephony Administrator",
	"Skype for Business Administrator",
	"Intune Administrator",
	"Compliance Administrator",
	"Security Operator",
	"Power Platform Administrator",
	"Dynamics 365 Administrator",
	"AI Administrator",
	"Global Secure Access Administrator",
	"Attribute Assignment Administrator",
	"Attribute Provisioning Administrator",
	"B2C IEF Keyset Administrator",
	"Cloud App Security Administrator",
	"External ID User Flow Administrator",
	"Partner Tier1 Support",
	"Windows 365 Administrator",
	"Microsoft 365 Backup Administrator",
	"Microsoft 365 Migration Administrator",
	"Yammer Administrator",
	"Knowledge Administrator",
	"Billing Administrator",
// ===== ENTRA TIER 2 (Medium – Read-only / restricted) =====
	"Global Reader",
	"Security Reader",
	"Attribute Provisioning Reader",
	"Application Developer",
	"Cloud Device Administrator",
	"Azure AD Joined Device Local Administrator"
]);
let PrivilegedUsers =
	IdentityInfo
	| where TimeGenerated >= ago(Lookback)
	| where AssignedRoles has_any (PrivilegedRoles)
	| summarize
		AssignedRoles  = make_set(AssignedRoles),
		Department	 = take_any(Department),
		JobTitle	   = take_any(JobTitle),
		AccountEnabled = take_any(IsAccountEnabled)
		by AccountUpn;
// Count per user + method
let AuthMethodCounts =
	SigninLogs
	| where TimeGenerated >= ago(Lookback)
	| where ResultType == 0
	| extend AuthDetails = parse_json(AuthenticationDetails)
	| extend AuthMethod = tostring(AuthDetails[0].authenticationMethod)
	| extend AuthMethodNorm = case(
		AuthMethod == "Password",							"🔑 Password",
		AuthMethod == "Mobile app notification",			"📱 Authenticator (Push)",
		AuthMethod == "Mobile app OTP",						"📱 Authenticator (OTP)",
		AuthMethod == "SMS",								"💬 SMS OTP",
		AuthMethod == "FIDO2 security key",					"🔐 FIDO2 / Passkey",
		AuthMethod == "Windows Hello for Business",			"🖥️ Windows Hello",
		AuthMethod == "Certificate-based authentication",	"📜 Certificate (CBA)",
		AuthMethod == "Voice call",							"📞 Phone Call",
		AuthMethod == "Temporary Access Pass",				"⏳ Temporary Access Pass",
		AuthMethod == "Previously satisfied",				"✅ SSO (Previous Session)",
		isempty(AuthMethod),								"❓ Unknown",
		AuthMethod
	)
	| summarize MethodCount = count() by UserPrincipalName, AuthMethodNorm;
// Build bag per user
let AuthMethodBags =
	AuthMethodCounts
	| summarize AuthMethodBreakdown = make_bag(bag_pack(AuthMethodNorm, MethodCount))
		by UserPrincipalName;
// Aggregate remaining metrics
let AuthEvents =
	SigninLogs
	| where TimeGenerated >= ago(Lookback)
	| where ResultType == 0
	| extend AuthDetails = parse_json(AuthenticationDetails)
	| extend AuthMethod = tostring(AuthDetails[0].authenticationMethod)
	| summarize
		TotalSignins		= count(),
		UniqueAuthMethods	= dcount(AuthMethod),
		LastSignin			= max(TimeGenerated),
		UniqueIPs			= dcount(IPAddress),
		UniqueApps			= dcount(AppDisplayName),
		MFASuccess			= countif(AuthenticationRequirement == "multiFactorAuthentication"),
		PasswordOnlySignins	= countif(AuthMethod == "Password" and AuthenticationRequirement == "singleFactorAuthentication")
		by UserPrincipalName;
// Combine data
PrivilegedUsers
| join kind=leftouter AuthEvents
	on $left.AccountUpn == $right.UserPrincipalName
| join kind=leftouter AuthMethodBags
	on $left.AccountUpn == $right.UserPrincipalName
| project
	User				= AccountUpn,
	Roles				= AssignedRoles,
	AccountActive		= AccountEnabled,
	SigninCount			= coalesce(TotalSignins, 0),
	UsedMethodsCount	= coalesce(UniqueAuthMethods, 0),
	AuthMethodsDetail	= AuthMethodBreakdown,
	LastSignin			= LastSignin,
	UniqueIPs			= coalesce(UniqueIPs, 0),
	UniqueApps			= coalesce(UniqueApps, 0),
	MFASignins			= coalesce(MFASuccess, 0),
	PasswordOnlySignins	= coalesce(PasswordOnlySignins, 0),
	RiskIndicator		= case(
		coalesce(PasswordOnlySignins, 0) > 0, "⚠️ Password-only sign-ins present",
		coalesce(TotalSignins, 0) == 0,	   "🔴 No sign-ins in 30 days",
		coalesce(UniqueAuthMethods, 0) == 1,  "🟡 Only one auth method active",
											  "🟢 OK"
	)
| where SigninCount > 0
| sort by SigninCount desc
```

Explanation

This query is designed to audit the authentication practices of highly privileged Azure Active Directory (Entra ID) accounts over the past 30 days. It focuses on identifying and assessing the security of these accounts by examining their sign-in activities. Here's a simplified breakdown of what the query does:

  1. Define Privileged Roles: It lists various roles considered privileged, categorized into three tiers based on their level of access and control.

  2. Identify Privileged Users: It checks for users who have been assigned any of these privileged roles within the last 30 days.

  3. Analyze Authentication Methods: It examines the sign-in logs to determine which authentication methods were used by these users, such as passwords, mobile app notifications, SMS OTP, etc.

  4. Summarize Sign-in Activity: It gathers statistics on the number of sign-ins, the diversity of authentication methods used, the last sign-in time, and the number of unique IPs and applications accessed.

  5. Assess MFA Usage: It counts how many sign-ins involved multi-factor authentication (MFA) and flags any password-only sign-ins.

  6. Evaluate Account Risk: It assigns a risk indicator to each account based on certain criteria, such as the presence of password-only sign-ins, inactivity over 30 days, or limited authentication methods.

  7. Output Results: It presents the findings in a structured format, highlighting key details like user roles, sign-in counts, authentication methods used, and any potential risks.

Overall, this query helps organizations assess the security posture of their privileged accounts, identify potential vulnerabilities, and ensure compliance with best practices for authentication security.

Details

Benjamin Zulliger profile picture

Benjamin Zulliger

Released: June 29, 2026

Tables

IdentityInfoSigninLogs

Keywords

PrivilegedAccountsAuthenticationAzureADSigninActivityMulti-FactorAuthenticationPasswordRiskAssessmentAccountHygieneSecurityRolesUsersMethodsIPsApps

Operators

letdynamicwherehas_anysummarizemake_settake_anybyextendparse_jsontostringcaseisemptycountsummarizemake_bagbag_packmaxdcountcountifjoinkindonprojectcoalescesort

Actions