A quarterly access review runs against your most sensitive group. Call it Azure Subscription Owners. The review starts, the reviewer is the group owner, and an email lands in their inbox asking them to confirm who still needs the role. The owner is on extended leave. Nobody covers the mailbox. The review's 14-day window opens, ticks down, and closes with exactly zero decisions recorded. Three weeks later a security audit finds a contractor who left the company in the spring still holding Owner on a production subscription. You pull up the access review to see what went wrong, and it says, in green: Completed. It did not fail. It did precisely what it was configured to do.
The assumption that trips almost everyone up is this: "if nobody reviews the access, the safe default kicks in and the access is removed." For a lot of tenants, that is exactly backwards. Depending on two settings on the review definition, an unreviewed principal can be auto-approved when the clock runs out, and keep their access indefinitely. This post walks through the exact mechanism, the precise property values that cause it, a one-item manual check you can do in two minutes, and a read-only PowerShell audit that flags every review in your tenant that behaves this way.
The problem: a review that "completed" is not the same as a review that removed anything
Here is the sequence, in order, because the order is the whole point. A recurring access review is created on a privileged group. Two boxes were ticked at creation time that nobody thought hard about: Auto apply results to resource, and an If reviewers don't respond value of Approve access. Both are legitimate, supported options in the Microsoft Entra admin center. Neither throws a warning that says "this will keep stale access".
The review runs. The reviewer never opens the mail. The instance duration ends. Because auto-apply is on, Entra does not wait for a human to click Apply - it applies decisions itself the moment the window closes. Because a default decision is enabled and set to Approve, every principal that no reviewer touched receives an Approve. The departed contractor is one of those principals. Their access is not merely left in place by inaction; it is actively re-approved by the system and recorded as an approved decision, complete with a decision reason along the lines of "Access approved automatically because reviewers did not respond." The review's status rolls to Completed, and the tenant looks perfectly governed.
This is more dangerous than a review that simply fails, because a failure at least leaves a red mark somewhere. This leaves a green one. The control that was supposed to catch stale privileged access has instead laundered it into a documented approval, and the audit trail will show that access was "reviewed and approved" on a date when, in reality, not one person looked at it.
Why it happens: three settings, and only one dangerous combination
Access reviews are configured through an accessReviewScheduleDefinition object, and the behaviour above is driven entirely by its settings block (an accessReviewScheduleSettings object). Three properties decide what happens to a principal nobody reviews:
autoApplyDecisionsEnabled(Boolean). Per Microsoft: decisions "are applied automatically after the access review instance duration ends, whether or not the reviewers have responded." When this isfalse, an admin must apply results by hand, so nothing changes on its own.defaultDecisionEnabled(Boolean). "Indicates whether the default decision is enabled or disabled when reviewers do not respond." This is the switch that says "use a fallback for anyone not reviewed".defaultDecision(String). The fallback itself. Microsoft documents exactly three allowed values:Approve,Deny, orRecommendation.
Microsoft's own reference carries a caution on both boolean properties, and it is worth reading precisely because it warns about the opposite failure mode to ours:
autoApplyDecisionsEnabled and defaultDecisionEnabled are true, all access for the principals to the resource risks being revoked if the reviewers fail to respond." That warning assumes your default decision is Deny. Flip the default to Approve and the identical configuration produces the inverse, unwarned outcome: all unreviewed access is retained. Same two booleans, opposite blast radius, and only the revoke case gets a caution in the docs.The cleanest way to reason about it is to map the four choices in the portal's If reviewers don't respond dropdown to the underlying property values, and then ask what happens to an unreviewed user when auto-apply is switched on:
| Portal: "If reviewers don't respond" | defaultDecisionEnabled | defaultDecision | With auto-apply ON, an unreviewed user... |
|---|---|---|---|
| No change | false | (none) | keeps access - no decision is applied to them |
| Approve access | true | Approve | keeps access - actively auto-approved |
| Remove access | true | Deny | loses access - this is the security-first choice |
| Take recommendations | true | Recommendation | keeps access if recently active; loses it if inactive |
Two of those four rows keep an unreviewed user's access. Approve access does it unconditionally. Take recommendations does it for anyone who is still signing in: the recommendation engine recommends Approve for a principal who has signed in during the look-back window and Deny only for one who has been inactive. So a departed employee's disabled-but-not-removed account might get a Deny recommendation, but a dormant service account, a shared break-glass identity, or a contractor who still logs in occasionally sails straight through on an auto-approved recommendation.
It also helps to be precise about what actually gets recorded. Because the default decision is applied rather than left blank, an unreviewed principal does not sit in a visible "not reviewed" limbo that a dashboard could flag - the review closes with an explicit Approve decision on that principal, dated to the day the window shut. That is the crux of why this is so easy to miss: the misconfiguration does not leave an obvious gap for anyone to notice. It produces a tidy, completed review whose recorded outcome is, at a glance, indistinguishable from one a diligent reviewer actually worked through line by line.
One more nuance that matters for accuracy: multi-stage reviews store some of their configuration per stage, in a stageSettings collection, and where that is present it can override the definition-level settings. The audit script below reports whether a flagged review uses multi-stage settings, so you know to confirm that one in the portal rather than trusting the top-level block alone.
And it is not only administrators who can introduce this. Where the tenant has switched on "Group owners can create and manage access reviews for groups they own", a business group owner with no security background can spin up a review on their own privileged group and pick Approve access as the no-response default purely because it reads like the least disruptive option in the dropdown. The riskiest setting in the whole flow is one click away for anyone holding that delegation, which is exactly why a tenant-wide audit beats trusting that whoever created each review knew what the option meant.
How to verify: read one review's settings before you trust anything
Before running a tenant-wide script, confirm the behaviour on a single review you already know about. There are two ways, and doing both once is the fastest way to build trust in the script's output. Start in the portal so you can see the human-readable version of the setting.
Open the review, look at Upon completion settings, and read two things: is Auto apply results to resource ticked, and what is If reviewers don't respond set to? If it is Approve access (or Take recommendations) with auto-apply on, you have found a live instance of the problem. Here is roughly what that panel looks like when it is misconfigured:
Then confirm the same thing programmatically, so you know exactly which property names map to what you just saw. This one-liner reads a single definition by ID and prints only the three properties that matter:
Get-MgIdentityGovernanceAccessReviewDefinition -All, but to fetch one you must pass -AccessReviewScheduleDefinitionId - the parameter is not called -Id. Note too that the List call does not expand associated instances; the definition's settings are what you inspect for this audit, and that is exactly what the list returns.The fix: Get-AccessReviewAutoApproveRisk.ps1
Reading every review by hand does not scale past a handful, and the misconfiguration is easy to reintroduce the next time someone clones a review template. The companion script enumerates every access review definition in the tenant, inspects the three settings above, and flags the ones that keep access on no-response. It classifies Approve plus auto-apply as High risk (access kept unconditionally) and Recommendation plus auto-apply as Medium (active users kept), and it tells you which findings use multi-stage settings so you can confirm those in the portal.
Get-* and connects with the least-privilege AccessReview.Read.All scope. It never creates, edits, applies, resets, stops, or deletes a review or a decision - it cannot change your no-response behaviour, only report it. Remediation is a deliberate human action in the portal, described in the next section.Download Get-AccessReviewAutoApproveRisk.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required. It is read-only and cannot change anything in your tenant — but always validate any script in your own environment before you rely on it.
Here is a first run against a tenant with the misconfigured review from earlier. The output is deliberately blunt about which state each risky review is in:
The classification logic is intentionally narrow so it does not cry wolf. A default decision only bites when it is both enabled and auto-applied, so the script requires autoApplyDecisionsEnabled and defaultDecisionEnabled to both be true before it looks at the decision value at all. A review with auto-apply off is left alone, because nothing happens there without a human clicking Apply.
The error handling is fail-loud on purpose. If the enumeration call to Graph throws - a missing scope, a throttle, an expired certificate - the script sets an error flag, prints the failure, and exits with code 1. It will never print "0 risky reviews found" off the back of a query that did not actually run, because a false clean bill of health on a privileged-access control is worse than no report at all. Exit codes are 0 for clean, 1 for an error, and 2 when findings exist, so you can wire it into a scheduled task or pipeline and alert on a non-zero result. Pass -ExportCsv to drop the findings to a timestamped file for evidence.
Proof it worked: re-run after fixing the no-response default
Remediation is a portal action, not a script one. For the Azure Subscription Owners review, the fix is to open the review's Settings and change If reviewers don't respond from Approve access to No change (leave existing access untouched but stop auto-approving the unreviewed) or, for a genuinely high-risk group, to Remove access so silence removes access rather than granting it. For a recurring review, remember to make the change on the Series settings, not just the current instance, or the next recurrence reverts to the old behaviour. Then re-run the audit:
Schedule the audit to run on the same cadence as your longest review recurrence - monthly is a sensible floor - so a review that gets re-templated with the risky default is caught within one cycle rather than one audit season. Pair it with the native completion notifications (the At end of review, send notification to option) so a human is told when a review closes, and the "nobody was watching" precondition that makes this whole failure possible stops being true.
References
- accessReviewScheduleSettings resource type (defaultDecision, defaultDecisionEnabled, autoApplyDecisionsEnabled) — Microsoft Graph
- accessReviewScheduleDefinition resource type — Microsoft Graph
- Create an access review of groups and applications (Upon completion settings; If reviewers don't respond) — Microsoft Entra ID Governance
- Get-MgIdentityGovernanceAccessReviewDefinition (AccessReview.Read.All) — Microsoft Graph PowerShell
- What are access reviews? — Microsoft Entra ID Governance
When did you last actually read the If reviewers don't respond setting on your most privileged access reviews, rather than assuming silence removes access? Run the audit against your tenant and see whether any of your governance controls have been quietly keeping access this whole time - I'd genuinely like to hear how many High findings turn up on Tier-0 groups.