prowler-cloud/prowler

[New Check]: Conditional Access excluded objects must be covered by another policy (no exclusion gaps)

Open

#11062 opened on May 6, 2026

View on GitHub
 (0 comments) (0 reactions) (0 assignees)Python (8,957 stars) (1,322 forks)batch import
feature-requestgood first issuenew-checkprovider/m365

Description

Existing check search

  • I have searched existing issues, Prowler Hub, and the public roadmap, and this check does not already exist.

Provider

Microsoft 365

New provider name

No response

Service or product area

entra

Suggested check name

entra_conditional_access_policy_no_exclusion_gaps

Context and goal

  • Security condition to validate: For every enabled Conditional Access policy, every object listed in any exclude* collection (excludeUsers, excludeGroups, excludeRoles, excludeApplications, excludeServicePrincipals, excludeLocations, excludePlatforms) must appear in at least one include* collection of some enabled Conditional Access policy. Excluded objects that never appear as included anywhere are "gaps" — they sit completely outside the CA control plane.
  • Why it matters: Excluding a principal from a policy is a common and legitimate operation (break-glass accounts, the Directory Synchronization Accounts role, certain service principals, etc.). It only stays safe when the excluded principal is also covered by another CA policy that enforces compensating controls. Otherwise the exclusion silently removes the principal from CA enforcement entirely, which is exactly how MFA bypass and lateral movement against admin accounts have happened in real incidents.
  • Resource involved: Microsoft Entra Conditional Access policies and their inclusion/exclusion collections (users, groups, roles, applications, service principals, locations, platforms).

Expected behavior

  • Resource or scope to evaluate: All Conditional Access policies whose state is enabled. Build a global "include set" by union-ing every include* collection across all enabled policies (per object type). For each enabled policy, walk every exclude* collection and compare each entry against the include set of the same type.
  • PASS when: every excluded object identifier exists in the global include set for its corresponding object type. Also PASS when no enabled policy exists or no policy uses any exclusion.
  • FAIL when: at least one excluded object identifier does not appear in the global include set of its type. The finding should report the orphaned object IDs grouped by type (users / groups / roles / apps / SPs / locations / platforms) and the policies that excluded them.
  • MANUAL when: not applicable.
  • Exclusions / edge cases:
    • Skip the Directory Synchronization Accounts role exclusion (template ID d29b2b05-8046-44ba-8758-1e26182fcf32) — Prowler already enforces this exclusion in entra_conditional_access_policy_directory_sync_account_excluded, and it is intended to have no fallback.
    • Skip exclusions that resolve to confirmed emergency-access accounts (the same accounts validated by entra_emergency_access_exclusion); they are intentional gaps.
    • Treat report-only policies as out of scope: only state = enabled policies count for both the exclusion side and the include set, mirroring real enforcement.
    • Group identifiers should be matched recursively when feasible (i.e. an exclusion of group A is "covered" if A is included; transitive group expansion is out of scope for v1).

References

Suggested severity

Medium

Additional implementation notes

  • Existing patterns to follow: Reuse the Conditional Access iteration pattern from entra_conditional_access_policy_mfa_enforced_for_guest_users and entra_emergency_access_exclusion (prowler/providers/m365/services/entra/).
  • Service change (entra_service.py): the ConditionalAccessPolicy model already exposes includeUsers/Groups/Roles and excludeUsers/Groups/Roles; verify and add (if missing) includeApplications/excludeApplications, includeServicePrincipals/excludeServicePrincipals (under clientApplications), includeLocations/excludeLocations, includePlatforms/excludePlatforms. No new Graph endpoint needed; the policy listing already returns these collections.
  • Permissions / scopes: No additional permissions beyond Prowler's M365 baseline (Policy.Read.All already grants read access to all Conditional Access policies).
  • PowerShell is NOT needed; this check uses the existing Microsoft Graph v1.0 data already loaded by the entra service.
  • Related checks (do not duplicate):
    • entra_conditional_access_policy_directory_sync_account_excluded — wants the dir-sync role excluded with no fallback. Add it as a hard skip in this check.
    • entra_emergency_access_exclusion — wants emergency accounts excluded. Treat them as expected gaps.
  • Metadata: follow the M365 metadata schema used by sibling checks under entra/.

Contributor guide