Skip to content

feat(m365/entra): add entra_conditional_access_policy_no_deleted_object_references check#11236

Open
ernestprovo23 wants to merge 2 commits into
prowler-cloud:masterfrom
ernestprovo23:fix/issue-11063-ca-deleted-object-refs
Open

feat(m365/entra): add entra_conditional_access_policy_no_deleted_object_references check#11236
ernestprovo23 wants to merge 2 commits into
prowler-cloud:masterfrom
ernestprovo23:fix/issue-11063-ca-deleted-object-refs

Conversation

@ernestprovo23
Copy link
Copy Markdown

Context

Conditional Access policies that reference deleted users, groups, or roles silently change runtime enforcement: stale include* references shrink scope, stale exclude* references can alter exemption logic. This is a well-known root cause of "we thought MFA was required but it wasn't" incidents. This PR adds a Prowler check that surfaces those stale references.

Fix #11063

Description

  • New check entra_conditional_access_policy_no_deleted_object_references iterates every Conditional Access policy (regardless of state, per the issue spec) and reports identifiers in includeUsers/excludeUsers/includeGroups/excludeGroups/includeRoles/excludeRoles that no longer resolve in Microsoft Entra ID.
  • Architecture mirrors feat(m365/entra): add entra_app_registration_client_secret_unused check (consolidates #11097 and #11212) #11232. Resolution runs eagerly at service-init time, not from inside the check. After the main asyncio.gather in Entra.__init__, a second loop.run_until_complete calls _resolve_directory_object_references, which walks the just-loaded conditional_access_policies, deduplicates referenced identifiers per type, skips the sentinel values All, None, and GuestsOrExternalUsers, and queries Microsoft Graph for each one. The result is cached on the entra client as unresolved_directory_object_references: Set[Tuple[str, str]]. Sync checks read this attribute without making any Graph calls of their own.
  • Endpoints queried: /users/{id}, /groups/{id}, /roleManagement/directory/roleDefinitions/{id}. No new permissions beyond Prowler's M365 baseline (Directory.Read.All).
  • Error handling: Only HTTP 404 (status code or Request_ResourceNotFound error code) flags an identifier as deleted. Transient errors (5xx, throttling, network failures) are logged and skipped — they must not produce false-positive FAILs per the spec.
  • Severity: Medium, Category identity-access + e3, ResourceGroup IAM.

Steps to review

  1. Check skeleton under prowler/providers/m365/services/entra/entra_conditional_access_policy_no_deleted_object_references/ follows the layout of the recently merged entra_app_registration_client_secret_unused check (PR feat(m365/entra): add entra_app_registration_client_secret_unused check (consolidates #11097 and #11212) #11232).
  2. Service-layer changes in entra_service.py:
    • Added Set to the typing import.
    • Added _resolve_directory_object_references and helper _resolve_identifiers_for_type methods.
    • Wired a second-phase loop.run_until_complete in Entra.__init__ between the main attribute unpacking and the if created_loop cleanup.
    • No existing fetchers were modified — purely additive.
  3. Unit tests (tests/providers/m365/services/entra/entra_conditional_access_policy_no_deleted_object_references/) cover 8 scenarios: no policies; sentinel-only references; all references resolve; deleted user in include; deleted group in exclude; deleted role in a disabled policy (still FAILs); orphans of all three types in one policy; multi-policy mixed PASS/FAIL. Tests follow the merged-PR pattern — pure unittest.mock against entra_client, no Graph mocking in check tests.
  4. CHANGELOG entry added under 5.28.0 Added.

Full M365 regression suite was run locally (pytest tests/providers/m365/) — 841 passed, 0 failures.

Checklist

Community Checklist
  • Review if the code is being covered by tests — 8 unit tests covering every documented scenario.
  • Review if code is being documented following PEP-257 / Google Python Style — Google-style docstrings on all new classes and methods.
  • Review if backport is needed — not applicable for a new check.
  • Review if is needed to change the Readme.md — no, scope-targeted.
  • Ensure new entries are added to CHANGELOG.md.

SDK/CLI

  • New checks included in this PR: Yes
  • Provider permissions update needed: NoDirectory.Read.All (already required for the entra service) grants /users/{id}, /groups/{id}, and /roleManagement/directory/roleDefinitions/{id} reads.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ernestprovo23 ernestprovo23 requested a review from a team as a code owner May 19, 2026 14:44
@github-actions github-actions Bot added provider/m365 Issues/PRs related with the M365 provider metadata-review community Opened by the Community labels May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

Conflict Markers Resolved

All conflict markers have been successfully resolved in this pull request.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.04%. Comparing base (476e7d1) to head (c6e127d).
⚠️ Report is 6 commits behind head on master.

❗ There is a different number of reports uploaded between BASE (476e7d1) and HEAD (c6e127d). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (476e7d1) HEAD (c6e127d)
api 1 0
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11236      +/-   ##
==========================================
- Coverage   93.96%   88.04%   -5.93%     
==========================================
  Files         236      134     -102     
  Lines       34777     5804   -28973     
==========================================
- Hits        32678     5110   -27568     
+ Misses       2099      694    -1405     
Flag Coverage Δ
api ?
prowler-py3.10-m365 87.93% <91.66%> (?)
prowler-py3.11-m365 87.54% <90.47%> (?)
prowler-py3.12-m365 87.93% <91.66%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
prowler 88.04% <83.33%> (∅)
api ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ct_references check

Audits every Conditional Access policy for references to users, groups, or
directory roles that no longer resolve in Microsoft Entra ID. Stale references
silently change runtime policy behavior — include* references shrink
enforcement scope, exclude* references can alter exemption logic — which is
a known root cause of "we thought MFA was required but it wasn't" incidents.

The implementation follows the eager-init pattern from prowler-cloud#11232:

- A new resolver method `_resolve_directory_object_references` runs at service
  init time after the main `asyncio.gather`, walks every loaded Conditional
  Access policy, deduplicates identifiers per type, and queries Graph for each
  one (`/users/{id}`, `/groups/{id}`,
  `/roleManagement/directory/roleDefinitions/{id}`). Sentinel values (`All`,
  `None`, `GuestsOrExternalUsers`) are skipped before any Graph call.
- Only HTTP 404 responses (or `Request_ResourceNotFound` error code) flag an
  identifier as deleted. Transient errors (5xx, throttling) are logged and
  skipped so the check never produces false positives on network blips.
- Result is cached on the entra client as
  `unresolved_directory_object_references: Set[Tuple[str, str]]`, which the
  new pure-sync check reads to emit findings.
- The check runs against all policies regardless of state — disabled-policy
  references are a misconfiguration that becomes live the moment the policy
  is re-enabled.

Fix prowler-cloud#11063
…ANGELOG

Completes prowler-cloud#11063 by:
- Adding `_resolve_directory_object_references` and helper
  `_resolve_identifiers_for_type` to `entra_service.py`.
- Hooking the resolver into `Entra.__init__` as a second-phase
  `loop.run_until_complete` after the main asyncio.gather, populating
  `self.unresolved_directory_object_references: Set[Tuple[str, str]]`.
- Adding the CHANGELOG entry for the new check.

Fix prowler-cloud#11063
@ernestprovo23 ernestprovo23 force-pushed the fix/issue-11063-ca-deleted-object-refs branch from c6e127d to 83f8c0b Compare May 19, 2026 16:59
@ernestprovo23
Copy link
Copy Markdown
Author

Quick update — I rebased onto current master to pull in #11234 (the Dockerfile uv sync --locked fix). The previous run's only failure was sdk-container-build-and-scan which was the same upstream issue #11234 resolved on master.

The rebase introduced zero code changes (only the new master commits land underneath the two feature commits). Could a maintainer kindly approve the workflow runs so CI can validate? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Opened by the Community metadata-review provider/m365 Issues/PRs related with the M365 provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant