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:

Prerequisites

Before running the script, ensure you have:

  1. Microsoft.Graph PowerShell module installed
  2. Appropriate permissions in your Entra ID tenant. I recommend using the Identity Governance Administrator role.
  3. Azure PowerShell module for authentication

Key Features

The script supports configuring:

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"