Add hunting queries: Entra privileged-identity utilization pack (3 queries) - #14813
Add hunting queries: Entra privileged-identity utilization pack (3 queries)#14813d4rk-pri0r wants to merge 2 commits into
Conversation
New SigninLogs hunting queries that surface licensed-but-minimally- configured identity gaps in Microsoft Entra ID: - DormantPrivilegedIdentities: enabled identities with directory or PIM roles that have no successful sign-in in 90 days - PrivilegedSigninsWithoutConditionalAccessSuccess: privileged users whose successful logons are not gated by an applied Conditional Access policy - PrivilegedAccountsUsingLegacyAuthentication: privileged users authenticating via legacy protocols that bypass modern CA/MFA All three join IdentityInfo (Microsoft 365 Defender) with SigninLogs and map Account/IP entities.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds three new Microsoft Entra ID SigninLogs hunting queries focused on identifying privileged identities showing risky/absent usage patterns in “licensed but minimally configured” tenants.
Changes:
- Added a query to find privileged sign-ins where Conditional Access did not succeed.
- Added a query to detect privileged accounts authenticating via legacy protocols.
- Added a query to surface privileged identities with no recent successful sign-ins.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| Hunting Queries/SigninLogs/PrivilegedSigninsWithoutConditionalAccessSuccess.yaml | New hunting query for privileged successful sign-ins lacking CA “success”. |
| Hunting Queries/SigninLogs/PrivilegedAccountsUsingLegacyAuthentication.yaml | New hunting query for privileged successful sign-ins using legacy client protocols. |
| Hunting Queries/SigninLogs/DormantPrivilegedIdentities.yaml | New hunting query for enabled privileged identities with no recent successful sign-ins. |
| let lookback = 90d; | ||
| let noSigninSince = 1d; | ||
| IdentityInfo | ||
| | where IsAccountEnabled == true | ||
| | where isnotempty(AssignedRoles) or isnotempty(PrivilegedEntraPimRoles) | ||
| | join kind=leftanti ( | ||
| SigninLogs | ||
| | where TimeGenerated > ago(noSigninSince) | ||
| | where ResultType == "0" | ||
| | project AccountObjectId = UserId | ||
| ) on AccountObjectId |
| entityMappings: | ||
| - entityType: Account | ||
| fieldMappings: | ||
| - identifier: FullName | ||
| columnName: AccountUpn | ||
| - identifier: Name | ||
| columnName: AccountName | ||
| - identifier: UPNSuffix | ||
| columnName: AccountUPNSuffix |
| | where isnotempty(ConditionalAccessStatus) | ||
| | where ConditionalAccessStatus !~ "success" |
| let PrivilegedUsers = IdentityInfo | ||
| | where isnotempty(AssignedRoles) or isnotempty(PrivilegedEntraPimRoles) | ||
| | project AccountObjectId, AccountUpn; |
| | where ResultType == "0" | ||
| | where isnotempty(ConditionalAccessStatus) | ||
| | where ConditionalAccessStatus !~ "success" | ||
| | join kind=inner PrivilegedUsers on $left.UserId == $right.AccountObjectId |
| let LegacyProtocols = dynamic([ | ||
| "IMAP", "POP", "SMTP", "ActiveSync", "ExchangeWebServices", | ||
| "Autodiscover", "OWA" | ||
| ]); |
| SigninLogs | ||
| | where TimeGenerated > ago(timeframe) | ||
| | where ResultType == "0" | ||
| | where ClientAppUsed has_any (LegacyProtocols) |
| let PrivilegedUsers = IdentityInfo | ||
| | where isnotempty(AssignedRoles) or isnotempty(PrivilegedEntraPimRoles) | ||
| | project AccountObjectId, AccountUpn; |
| | where TimeGenerated > ago(timeframe) | ||
| | where ResultType == "0" | ||
| | where ClientAppUsed has_any (LegacyProtocols) | ||
| | join kind=inner PrivilegedUsers on $left.UserId == $right.AccountObjectId |
| IPAddresses = make_set(IPAddress) | ||
| by UserPrincipalName, AccountName, AccountUPNSuffix, UserId, AccountUpn, AppDisplayName | ||
| | extend FirstIP = tostring(IPAddresses[0]) |
|
Hi @d4rk-pri0r, Please review the suggestions above and make the necessary changes accordingly. Also, please confirm that all queries are working correctly in your environment. If possible, share screenshots of the queries running successfully for verification. Thanks |
|
Addressed the Copilot review feedback across all three queries (v1.0.1): DormantPrivilegedIdentities
PrivilegedSigninsWithoutConditionalAccessSuccess
PrivilegedAccountsUsingLegacyAuthentication
CI was green on the prior revision; this is description/KQL hardening only. I do not have a live tenant screenshot handy for this environment — happy to add one if you still need visual confirmation after CI re-runs. |
Fix dormant lookback (use 90d), dedupe IdentityInfo, add AadUserId, include null CA status, align legacy ClientAppUsed values with repo patterns, and pick LastIP via arg_max for stable entity mapping.
Details
Three new SigninLogs hunting queries under the "licensed but minimally configured" theme, surfacing identities that hold directory or PIM roles yet show risky/absent usage patterns in Microsoft Entra ID:
AssignedRolesorPrivilegedEntraPimRolesthat have no successful sign-in in 90 days (blast radius ranked), a classic takeover target and unused-license signal.ConditionalAccessStatuswas notsuccess, i.e. logons reaching the tenant that were not gated by an applied Conditional Access policy.All three join
IdentityInfo(Microsoft 365 Defender) withSigninLogsand map Account and IP entities.Testing
kql-validations(runs on this PR).Note to reviewers
Author metadata:
d4rk-pri0r, Community support tier. If any column name is rejected by KQL validation, I'll patch the affected query.