Automating Entra ID Access Reviews with PowerShell
Introduction
Regular access reviews are crucial for maintaining security in Azure environments. In this post, I’ll share a PowerShell script that automates the creation of access reviews for Entra ID groups .
The Problem
Setting up access reviews manually through the Azure Portal can be time-consuming and error-prone, especially when you need to create multiple reviews with consistent settings. Additionally, manual creation makes it difficult to version control your access review configurations.
The Solution
Our PowerShell script automates the creation of access reviews using the Microsoft Graph API. It provides:
- Flexible configuration through parameters
- Validation of inputs
- Detailed logging
- Error handling
- Idempotent execution (won’t create duplicate reviews)
Prerequisites
Before running the script, ensure you have:
- Microsoft.Graph PowerShell module installed
- Appropriate permissions in your Entra ID tenant. I recommend using the Identity Governance Administrator role.
- Azure PowerShell module for authentication
Key Features
The script supports configuring:
- Review duration and recurrence
- Notification settings
- Auto-apply options
- Default decisions
- AI-based recommendations
- Justification requirements
The PowerShell script
<#
.SYNOPSIS
Creates an access review for an Entra ID group.
.DESCRIPTION
This script creates an automated access review schedule for an Entra ID group. It configures:
- Notification settings for reviewers
- Review duration and recurrence
- Auto-apply settings and recommendations
- Default decisions and justification requirements
.AUTHOR Øystein Stræte
.PARAMETER GroupName
The name of the Entra ID group to review.
.PARAMETER AccessReviewName
Name of the access review.
.PARAMETER MailNotificationsEnabled
Enable/disable email notifications. Default: True
.PARAMETER ReminderNotificationsEnabled
Enable/disable reminder notifications. Default: True
.PARAMETER JustificationRequiredOnApproval
Require reviewers to provide justification on approval. Default: True
.PARAMETER DefaultDecisionEnabled
Enable/disable default decision if no review completed. Default: False
.PARAMETER DefaultDecision
Default decision type if enabled. Default: "None"
.PARAMETER InstanceDurationInDays
Number of days each review instance should last. Default: 15
.PARAMETER RecommendationsEnabled
Enable/disable AI-based recommendations. Default: True
.PARAMETER AutoApplyDecisionsEnabled
Automatically apply decisions when review completes. Default: True
.PARAMETER RecurrenceIntervalMonths
How often the review should recur in months. Default: 6
.PARAMETER StartDateOffsetMonths
When the first review should start, in months from now. Default: 6
.EXAMPLE
.\Create-AccessReview.ps1 -GroupName "MyEntraGroup" -AccessReviewName "Security-Review-Q1"
.EXAMPLE
.\Create-AccessReview.ps1 -GroupName "MyEntraGroup" -AccessReviewName "Monthly-Review" -InstanceDurationInDays 30 -RecurrenceIntervalMonths 1
.EXAMPLE
.\Create-AccessReview.ps1 -GroupName "MyEntraGroup" -AccessReviewName "Manual-Review" -AutoApplyDecisionsEnabled $false -DefaultDecisionEnabled $true -DefaultDecision "Deny"
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true,
HelpMessage = "The name of the Landing Zone Entra ID Group.")]
[string]$GroupName,
[Parameter(Mandatory = $true,
HelpMessage = "The name of the Access Review that will be created.")]
[string]$AccessReviewName,
[Parameter(HelpMessage = "Enable mail notifications")]
[bool]$MailNotificationsEnabled = $true,
[Parameter(HelpMessage = "Enable reminder notifications")]
[bool]$ReminderNotificationsEnabled = $true,
[Parameter(HelpMessage = "Require justification on approval")]
[bool]$JustificationRequiredOnApproval = $true,
[Parameter(HelpMessage = "Enable default decision")]
[bool]$DefaultDecisionEnabled = $false,
[Parameter(HelpMessage = "Default decision type")]
[ValidateSet("None", "Accept", "Deny")]
[string]$DefaultDecision = "None",
[Parameter(HelpMessage = "Duration of review in days")]
[ValidateRange(1, 180)]
[int]$InstanceDurationInDays = 15,
[Parameter(HelpMessage = "Enable recommendations")]
[bool]$RecommendationsEnabled = $true,
[Parameter(HelpMessage = "Enable auto-apply decisions")]
[bool]$AutoApplyDecisionsEnabled = $true,
[Parameter(HelpMessage = "Recurrence interval in months")]
[ValidateRange(1, 12)]
[int]$RecurrenceIntervalMonths = 6,
[Parameter(HelpMessage = "Start date offset in months")]
[ValidateRange(0, 12)]
[int]$StartDateOffsetMonths = 6
)
# Stop on error.
$ErrorActionPreference = 'Stop'
# Check required modules
$requiredModules = @('Microsoft.Graph')
foreach ($module in $requiredModules) {
if (!(Get-Module -ListAvailable -Name $module)) {
Write-Error "Required module '$module' is not installed. Please install it using: Install-Module $module -Force"
exit 1
}
}
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Starting access review creation for group $GroupName"
try {
# Get access token for login
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Getting access token" -Verbose:$true
$token = (Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com').Token | ConvertTo-SecureString -AsPlainText -Force
# Connect to Microsoft Graph
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Connecting to Microsoft Graph"
Connect-MgGraph -NoWelcome -AccessToken $token
# Get the group from Entra ID
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Getting group $GroupName from Entra ID"
$group = Get-MgGroup -Filter "DisplayName eq '$GroupName'"
if (!$group) {
throw "A group with the name $GroupName doesn't exist."
}
# Get the group id
$groupId = $group.id
# Check if access review exists
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Checking if access review exists"
$existingAccessReview = Get-MgIdentityGovernanceAccessReviewDefinition -Filter "DisplayName eq '$AccessReviewName'"
if ($existingAccessReview) {
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Access review $AccessReviewName already exists"
exit 0
}
$params = @{
displayName = $AccessReviewName
descriptionForAdmins = "Semi-annual review of the members of the Entra ID group $GroupName"
descriptionForReviewers = "Review if the members of this group should continue to have access to the subscriptions and Azure DevOps project for the landing zone."
scope = @{
"@odata.type" = "#microsoft.graph.accessReviewQueryScope"
query = "/groups/$groupId/transitiveMembers"
queryType = "MicrosoftGraph"
}
reviewers = @(
@{
query = "/groups/$groupId/owners"
queryType = "MicrosoftGraph"
}
)
settings = @{
mailNotificationsEnabled = $MailNotificationsEnabled
reminderNotificationsEnabled = $ReminderNotificationsEnabled
justificationRequiredOnApproval = $JustificationRequiredOnApproval
defaultDecisionEnabled = $DefaultDecisionEnabled
defaultDecision = $DefaultDecision
instanceDurationInDays = $InstanceDurationInDays
recommendationsEnabled = $RecommendationsEnabled
autoApplyDecisionsEnabled = $AutoApplyDecisionsEnabled
recurrence = @{
pattern = @{
type = "absoluteMonthly"
interval = $RecurrenceIntervalMonths
}
range = @{
type = "noEnd"
startDate = "$((Get-Date).AddMonths($StartDateOffsetMonths).ToString("yyyy-MM-dd"))T12:02:30.667Z"
}
}
}
}
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Creating access review $AccessReviewName"
New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $params
Write-Verbose "[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] Access review creation completed successfully"
}
catch {
Write-Error "Error occurred: $_"
Write-Error $_.ScriptStackTrace
exit 1
}
exit 0
Usage Examples
Basic usage with required parameters:
.\Create-AccessReview.ps1 -GroupName "MyEntraGroup" -AccessReviewName "Security-Review-Q1"