Skip to content

[New Check]: Conditional Access policies must not reference deleted users, groups, or roles #11063

@HugoPBrito

Description

@HugoPBrito

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_deleted_object_references

Context and goal

  • Security condition to validate: Every object identifier referenced by any Conditional Access policy under conditions.users — namely includeUsers, excludeUsers, includeGroups, excludeGroups, includeRoles, excludeRoles — must resolve to an existing Microsoft Entra object.
  • Why it matters: When a user, group, or directory role referenced by a Conditional Access policy stops resolving in the directory (account deleted, group deleted, role template removed), the reference becomes orphaned. In include* collections this silently shrinks the policy's enforcement scope; in exclude* collections it can cause the policy to evaluate unexpectedly. Either way, the policy stops behaving the way the operator believes it does, which is one of the more common root causes of "we thought MFA was required but it wasn't" incidents.
  • Resource involved: Microsoft Entra Conditional Access policies and the users, security/Microsoft 365 groups, and directory role templates they reference.

Expected behavior

  • Resource or scope to evaluate: All Conditional Access policies in the tenant, regardless of state (enabled, disabled, and enabledForReportingButNotEnforced). Disabled policies still represent operator intent and a stale reference there is a misconfiguration that will go live the moment the policy is re-enabled. Build the deduplicated set of identifiers (per type) across all six collections — includeUsers, excludeUsers, includeGroups, excludeGroups, includeRoles, excludeRoles — for every policy, then resolve each identifier via Microsoft Graph using the type-appropriate endpoint:
    • Users → GET /users/{id}
    • Groups → GET /groups/{id}
    • Roles → GET /roleManagement/directory/roleDefinitions/{id}
  • PASS when: every referenced identifier resolves successfully (HTTP 200) on the v1.0 Graph endpoint corresponding to its type. Also PASS when no policy references any user, group, or role.
  • FAIL when: at least one referenced identifier returns HTTP 404 from its resolution endpoint. The finding should report each missing identifier together with its type (User / Group / Role), the policies that reference it, and the include vs exclude side.
  • MANUAL when: not applicable.
  • Exclusions / edge cases:
    • Treat any non-404 Graph error (5xx, throttling, transient network failure) as a check error, not a FAIL — do not flag an object as deleted on transient failures.
    • Cache resolved identifiers across the run so an object referenced by N policies costs one Graph call, not N. Cache per type.
    • Disabled policies still count: stale references in disabled policies are a misconfiguration that becomes live the moment the policy is enabled.
    • Sentinel values such as "All", "None", "GuestsOrExternalUsers" (used in includeUsers/excludeUsers) are not object identifiers and must be skipped before issuing a Graph lookup.

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. Add a small resolver helper in entra_service.py that takes a list of identifiers and a type (user / group / role), issues the corresponding GET /…/{id}?$select=id,displayName calls, and returns the set of identifiers that 404. Cache results within the run, keyed by (type, id).
  • Permissions / scopes: No additional permissions beyond Prowler's M365 baseline (Directory.Read.All, Policy.Read.All). Directory.Read.All already grants reads against /users/{id}, /groups/{id}, and the unified role-management endpoints used here.
  • PowerShell is NOT needed; this check uses Microsoft Graph v1.0 only.
  • Related checks (do NOT duplicate, complementary):
  • Metadata: follow the M365 metadata schema used by sibling checks under entra/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestNew feature request for Prowler.good first issueIndicates a good issue for first-time contributorsprovider/m365Issues/PRs related with the M365 provider

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions