← Back to SOC feed Coverage →

Privileged identities whose sign-ins are not protected by Conditional Access

kql MEDIUM Azure-Sentinel
T1078.004
IdentityInfoSigninLogs
huntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-08-13T11:00:01Z · Confidence: medium

Hunt Hypothesis

This hunt detects adversaries leveraging compromised privileged identities to bypass security controls by signing in without active Conditional Access protection, indicating a potential gap in identity governance (T1078.004). Proactively hunting for these unprotected sessions is critical because attackers often target high-privilege accounts with weak access policies to establish persistence and move laterally within the Azure environment undetected.

KQL Query

let timeframe = 30d;
let PrivilegedUsers = IdentityInfo
| where isnotempty(AssignedRoles) or isnotempty(PrivilegedEntraPimRoles)
| summarize arg_max(Timestamp, AccountUpn) by AccountObjectId;
SigninLogs
| where TimeGenerated > ago(timeframe)
| where ResultType == "0"
| extend ConditionalAccessStatus = coalesce(ConditionalAccessStatus, "notEvaluated")
| where ConditionalAccessStatus !~ "success"
| join kind=inner PrivilegedUsers on $left.UserId == $right.AccountObjectId
| extend
    AccountName = tostring(split(UserPrincipalName, "@")[0]),
    AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
| summarize
    SignInAttempts = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    ConditionalAccessStatuses = make_set(ConditionalAccessStatus),
    AuthRequirements = make_set(AuthenticationRequirement),
    LastIP = tostring(arg_max(TimeGenerated, IPAddress))
    by UserPrincipalName, AccountName, AccountUPNSuffix, UserId, AccountUpn, AppDisplayName
| project
    UserPrincipalName, AccountName, AccountUPNSuffix, AccountUpn,
    SignInAttempts, FirstSeen, LastSeen, ConditionalAccessStatuses,
    AuthRequirements, LastIP, AppDisplayName
| sort by SignInAttempts desc

Analytic Rule Definition

id: 87245d60-eefb-42dd-9748-cd1949c83a5e
name: Privileged identities whose sign-ins are not protected by Conditional Access
description: |
  Identifies successful sign-ins by accounts with directory or PIM roles when
  Conditional Access did not report success, including empty, not applied,
  not enabled, or failure states. Review these events for missing policy
  coverage and MFA enforcement.
description-detailed: |
  Joins privileged accounts from the Microsoft 365 Defender IdentityInfo
  table (AssignedRoles / PrivilegedEntraPimRoles) against SigninLogs,
  keeping only successful logons whose ConditionalAccessStatus is not
  'success' (including empty/null, notApplied, notEnabled, or failure).
  Review whether a policy intended to cover privileged roles is
  misconfigured, out of scope, or disabled (compare with the
  ConditionalAccessPolicyDisabled family of hunting queries in
  Hunting Queries/AuditLogs), and prioritize accounts that also
  authenticate without MFA (AuthenticationRequirement =
  singleFactorAuthentication).
  References:
  - https://learn.microsoft.com/entra/identity/conditional-access/overview
  - https://attack.mitre.org/techniques/T1078/004/
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - IdentityInfo
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
query: |
  let timeframe = 30d;
  let PrivilegedUsers = IdentityInfo
  | where isnotempty(AssignedRoles) or isnotempty(PrivilegedEntraPimRoles)
  | summarize arg_max(Timestamp, AccountUpn) by AccountObjectId;
  SigninLogs
  | where TimeGenerated > ago(timeframe)
  | where ResultType == "0"
  | extend ConditionalAccessStatus = coalesce(ConditionalAccessStatus, "notEvaluated")
  | where ConditionalAccessStatus !~ "success"
  | join kind=inner PrivilegedUsers on $left.UserId == $right.AccountObjectId
  | extend
      AccountName = tostring(split(UserPrincipalName, "@")[0]),
      AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
  | summarize
      SignInAttempts = count(),
      FirstSeen = min(TimeGenerated),
      LastSeen = max(TimeGenerated),
      ConditionalAccessStatuses = make_set(ConditionalAccessStatus),
      AuthRequirements = make_set(AuthenticationRequirement),
      LastIP = tostring(arg_max(TimeGenerated, IPAddress))
      by UserPrincipalName, AccountName, AccountUPNSuffix, UserId, AccountUpn, AppDisplayName
  | project
      UserPrincipalName, AccountName, AccountUPNSuffix, AccountUpn,
      SignInAttempts, FirstSeen, LastSeen, ConditionalAccessStatuses,
      AuthRequirements, LastIP, AppDisplayName
  | sort by SignInAttempts desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: AadUserId
        columnName:

Required Data Sources

Sentinel TableNotes
IdentityInfoEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Here are 5 specific false positive scenarios for the rule “Privileged identities whose sign-ins are not protected by Conditional Access,” including suggested filters and exclusions:

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/SigninLogs/PrivilegedSigninsWithoutConditionalAccessSuccess.yaml