In this section

Conditional Access Evaluation in Sign-In Logs

4-5 hours · Module 1 · Free
What you already know

Section 1.4 showed you the AuthenticationDetails array — exactly which method a user used to prove their identity and whether that method was phishing-resistant. This section examines the next critical array in the sign-in log: ConditionalAccessPolicies. It records every Conditional Access policy that evaluated on a sign-in, what each one decided, and what controls were enforced. This is how you verify that the policies you deploy in EI3 and EI4 are actually working — not by checking the policy configuration, but by reading the evidence of what happened when a real user signed in.

Scenario

You deployed three Conditional Access policies last week: require MFA for all users, block legacy authentication, and require compliant devices for Exchange Online. Your CISO asks whether they're working. You open the Conditional Access blade — all three show "On." But "On" tells you the policy exists. It doesn't tell you whether the policy evaluated on the sign-ins it was supposed to catch, whether it enforced the controls you intended, or whether entire categories of sign-ins are slipping through without any policy evaluation at all. The ConditionalAccessPolicies array in the sign-in log answers all three questions.

What the ConditionalAccessPolicies array contains

Every sign-in log entry includes a ConditionalAccessPolicies field — a JSON array where each element represents one Conditional Access policy that was evaluated during that authentication event. The array is not limited to policies that applied. Every enabled policy in your tenant appears in the array for every sign-in, with a result field recording what happened. The five properties in each element are displayName (the policy name), id (the policy GUID), result (the evaluation outcome), enforcedGrantControls (what the policy required — MFA, compliant device, hybrid join), and enforcedSessionControls (session restrictions — sign-in frequency, persistent browser, app-enforced restrictions).

The result field carries one of seven values that determine what happened to the sign-in. Three are enforcement states: success means the policy's conditions matched and the user satisfied its controls. failure means the policy's conditions matched but the user did not satisfy its controls — access was blocked. notApplied means the policy's conditions did not match this sign-in, so the policy was not evaluated. Two are report-only states: reportOnlySuccess and reportOnlyFailure record what would have happened if the policy were enforced, without actually blocking or requiring anything. The remaining two — notEnabled (the policy is disabled) and unknown — are administrative states that rarely appear in operational analysis.

Here is what the array looks like for a sign-in where an MFA policy applied and a device compliance policy did not:

Sign-in Record
{
  "userPrincipalName": "p.sharma@contoso.com",
  "appDisplayName": "Microsoft Teams",
  "conditionalAccessStatus": "success",
  "conditionalAccessPolicies": [
    {
      "displayName": "CA001: Require MFA for All Users",
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "result": "success",
      "enforcedGrantControls": ["Mfa"],
      "enforcedSessionControls": []
    },
    {
      "displayName": "CA002: Require Compliant Device for Exchange",
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "result": "notApplied",
      "enforcedGrantControls": [],
      "enforcedSessionControls": []
    },
    {
      "displayName": "CA003: Block Legacy Authentication",
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "result": "notApplied",
      "enforcedGrantControls": [],
      "enforcedSessionControls": []
    }
  ]
}

Read this record carefully. CA001 evaluated and succeeded — Priya was challenged for MFA and completed it. The enforcedGrantControls array shows ["Mfa"], confirming the grant control that was applied. CA002 shows notApplied because its target resource is Exchange Online and Priya signed into Teams. CA003 shows notApplied because Priya used a modern authentication client, not a legacy protocol. Both notApplied results are expected — the policies are scoped correctly and the sign-in didn't match their conditions.

The enforcedGrantControls array is empty when a policy doesn't apply. When it does apply, the values tell you exactly which control was enforced: "Mfa" for multifactor authentication, "CompliantDevice" for device compliance, "DomainJoinedDevice" for hybrid join, "ApprovedApplication" for app protection, "CompliantApplication" for app compliance policy, "AuthenticationStrength" for authentication strength requirements. The enforcedSessionControls array shows session restrictions: "SignInFrequency" for sign-in frequency, "PersistentBrowser" for persistent browser sessions, "CloudAppSecurity" for Defender for Cloud Apps enforcement.

The four evaluation outcomes

CONDITIONAL ACCESS EVALUATION OUTCOMES SUCCESS Policy conditions matched. User satisfied controls. Access granted. enforcedGrantControls populated. The policy is working as designed ✓ FAILURE Policy conditions matched. User did NOT satisfy controls. Access blocked. ResultType 53003 on the sign-in. The policy blocked a non-compliant sign-in ✓ NOT APPLIED Policy conditions did not match this sign-in. User not in scope, app not targeted, or condition mismatch. Expected for out-of-scope sign-ins. Investigate if unexpected. ZERO POLICIES EVALUATED Top-level ConditionalAccessStatus == "notApplied" No policy in the entire tenant matched this sign-in. This sign-in bypassed your entire CA architecture ✗ REPORT-ONLY: reportOnlySuccess / reportOnlyFailure — policy evaluated but not enforced. Use to test before turning on enforcement. Safe testing ✓

Figure 1.5 — Conditional Access evaluation outcomes. The bottom-right state is the most dangerous — a successful sign-in with no policy evaluation means your CA architecture has a gap.

The distinction between individual-policy notApplied and sign-in-level ConditionalAccessStatus == "notApplied" is critical. When a single policy shows notApplied, that policy's conditions didn't match — which is normal. Every policy has a scope, and most sign-ins won't match every policy. The danger is when the top-level ConditionalAccessStatus field on the sign-in itself shows notApplied, meaning no policy in the entire tenant evaluated for that authentication event. That sign-in bypassed your complete CA architecture. No MFA requirement, no device compliance check, no session restriction. This is the Zero Trust gap that matters most.

Finding sign-ins with no policy evaluation

The most important query in this section finds successful sign-ins that no Conditional Access policy evaluated. Every result is a gap in your security architecture.

KQL
// Find successful sign-ins evaluated by ZERO conditional access policies
// Every result is a gap in your CA architecture
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where ConditionalAccessStatus == "notApplied"
| summarize
    GapCount = count(),
    Users = make_set(UserPrincipalName, 10),
    Apps = make_set(AppDisplayName, 10)
    by bin(TimeGenerated, 1h)
| where GapCount > 0
| order by GapCount desc

The common causes are predictable once you know what to look for. Applications not targeted by any policy — Azure DevOps, Power BI, custom LOB apps, and third-party SaaS applications are frequently absent from CA policy target resource lists. User groups excluded from all policies — break-glass accounts are intentionally excluded, but service accounts and guest users are often unintentionally excluded. Client types that don't match any condition — mobile devices on a platform not specified in any policy's device condition. Each cause has a different fix, and EI3 teaches the systematic approach to closing them.

Run this query after deploying policies in EI3 and EI4. Run it weekly as part of the operational monitoring cadence in EI14. If you're in a developer tenant with no CA policies configured, every successful sign-in will return ConditionalAccessStatus == "notApplied" — that's your baseline of 100% gap. By the time you complete EI4, this number should approach zero for interactive user sign-ins.

Entra Admin Center

IdentityMonitoring & healthSign-in logs → click any entry → Conditional Access tab
The portal shows each policy's evaluation result in a formatted table. The Result column shows success, failure, or not applied. The Grant Controls and Session Controls columns show what was enforced. For any sign-in where the Result column shows "Not applied" for every policy, you've found a gap — that sign-in was not protected by any CA policy. The KQL query scales this across thousands of sign-ins; the portal view confirms it for a single event during investigation.

Verifying a specific policy

Deploying a Conditional Access policy is not the end — it's the beginning of verification. Within the first 24–48 hours of deployment, you should confirm the policy is evaluating on the sign-ins you intended, producing the outcomes you expected, and not blocking users who should have access.

The verification pattern uses mv-expand to flatten the ConditionalAccessPolicies array into individual rows — one per policy per sign-in — then filters by policy name to see how that specific policy behaved across all sign-ins in the time window.

KQL
// Verify a specific CA policy is evaluating correctly
let policyName = "CA001: Require MFA for All Users";
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| mv-expand CAPolicy = parse_json(ConditionalAccessPolicies)
| where tostring(CAPolicy.displayName) == policyName
| extend PolicyResult = tostring(CAPolicy.result)
| extend GrantControls = tostring(CAPolicy.enforcedGrantControls)
| summarize
    Evaluated = count(),
    Success = countif(PolicyResult == "success"),
    Failure = countif(PolicyResult == "failure"),
    NotApplied = countif(PolicyResult == "notApplied"),
    ReportOnly = countif(PolicyResult has "reportOnly")
    by bin(TimeGenerated, 1h)
| order by TimeGenerated asc

The mv-expand operator is the key to working with the ConditionalAccessPolicies array. Without it, the array is an opaque JSON blob — you can see that policies exist, but you can't filter, aggregate, or analyze individual policy evaluations across thousands of sign-ins. With it, each row becomes one policy evaluation for one sign-in, and you can use standard KQL filtering and aggregation to answer any question about that policy's behavior.

Three results from this query tell you everything you need to know. If Evaluated is zero for any hour during business hours, the policy is not matching any sign-ins — check the assignment scope, target resources, and conditions. If Success is consistently high and Failure is low, the policy is enforcing and users are complying. If Failure is unexpectedly high, users are being blocked — either the policy scope is too broad or users need remediation (device enrollment, MFA registration) before the policy works as intended.

What we see in 90% of environments

The CA policy blade shows three policies in "On" state. The administrator reports "Conditional Access is configured and enforcing." Nobody has queried the sign-in logs to verify evaluation. The gap-finding query reveals that 40% of successful sign-ins have ConditionalAccessStatus == "notApplied" — none of the three policies target the applications those sign-ins access. The policies are on, but they're protecting three applications out of thirty. The other twenty-seven are unprotected. The CA blade doesn't show you what isn't covered — only the sign-in log does.

Report-only mode evaluation

Conditional Access policies deployed in report-only mode evaluate against every matching sign-in without enforcing controls. The evaluation appears in the ConditionalAccessPolicies array with result values reportOnlySuccess (the user would have satisfied the controls) and reportOnlyFailure (the user would have been blocked). This is the safe deployment path — you see exactly which sign-ins would be affected before you turn on enforcement.

The query that tells you whether a report-only policy is safe to enforce:

KQL
// Analyze report-only policy impact before enforcement
SigninLogs
| where TimeGenerated > ago(7d)
| mv-expand CAPolicy = parse_json(ConditionalAccessPolicies)
| where tostring(CAPolicy.result) has "reportOnly"
| extend PolicyName = tostring(CAPolicy.displayName)
| extend PolicyResult = tostring(CAPolicy.result)
| summarize
    WouldSucceed = countif(PolicyResult == "reportOnlySuccess"),
    WouldBlock = countif(PolicyResult == "reportOnlyFailure")
    by PolicyName
| extend BlockPercentage = round(100.0 * WouldBlock / (WouldSucceed + WouldBlock), 1)
| order by BlockPercentage desc

If BlockPercentage is zero, every matching sign-in would have satisfied the controls — the policy is safe to enforce without disrupting any user. If it's 15%, that means 15% of matching sign-ins would be blocked. Those users either need remediation (enroll their device, register for MFA) or an exception before you enforce the policy. EI8 covers the full report-only testing and promotion workflow. For now, the key skill is reading the report-only results in the sign-in log and understanding that this query is the safety check you run before every enforcement decision.

Combining CA evaluation with risk and authentication data

The ConditionalAccessPolicies array becomes most valuable when you combine it with the fields from Sections 1.4 and 1.6 — authentication method strength and risk signals. Three cross-field patterns expose the most dangerous gaps in a CA architecture.

The first pattern finds risky sign-ins that bypassed CA entirely. A sign-in flagged as medium or high risk by Identity Protection that was not evaluated by any Conditional Access policy means the risk signal was detected but no enforcement action was taken — the equivalent of a fire alarm sounding in an empty building.

The second pattern finds sign-ins where MFA was required but satisfied with a phishing-capable method. The CA policy required MFA and the user completed it — but with a push notification rather than a phishing-resistant method. The policy's result shows success, which looks correct. But combining the CA evaluation with the AuthenticationDetails array from Section 1.4 reveals that the "success" was achieved with a method vulnerable to AiTM relay. The policy enforced a control. The control wasn't strong enough.

The third pattern finds sign-ins from non-compliant devices that succeeded. If your policy requires compliant devices for Exchange Online and SharePoint, this pattern finds sign-ins to those applications where the device was not compliant but access was granted anyway — indicating the user or application was excluded from the compliance requirement, or the sign-in was to a resource not covered by the policy. The CA evaluation shows notApplied on the compliance policy, and the DeviceDetail.isCompliant field shows false. Together, they prove the gap exists.

These cross-field patterns are the queries that EI13 turns into automated detection rules. For now, the skill is recognizing that no single field in the sign-in log tells the full story. The CA evaluation tells you what policies decided. The AuthenticationDetails tell you how the user authenticated. The risk fields tell you how suspicious the sign-in looked. The security picture emerges from reading them together.

Identity Security Principle

A Conditional Access policy that is "On" is not a policy that is working. A policy that is working is one where the sign-in logs show it evaluated on the sign-ins you intended, enforced the controls you designed, and left no category of sign-in unprotected. The ConditionalAccessPolicies array is the evidence — not the policy blade, not the configuration screen, not the status indicator. Evidence in the log, or it didn't happen.

Next

Section 1.6 examines the risk signals that Identity Protection attaches to sign-in events — the RiskLevelDuringSignIn, RiskLevelAggregated, and RiskEventTypes_V2 fields that classify how suspicious a sign-in looks. You'll learn which risk detections are real-time versus offline, how to query risk data at scale, and how to assess detection accuracy before deploying risk-based Conditional Access policies in EI5.

Unlock the Full Course See Full Course Agenda