Entra / Microsoft 365 · Groups
Get Entra ID access review details (Graph)
Navigate the Microsoft Graph API for Entra ID access reviews, including reviews scoped to all groups and Teams.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
# Review required modules and connection steps before running.# Connect to Microsoft Graph or Exchange Online as needed for this script.
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "",[string] $AppId = "")function Get-GraphData {# GET data from Microsoft Graph.# Based on https://danielchronlund.com/2018/11/19/fetch-data-from-microsoft-graph-with-powershell-paging-support/param ([parameter(Mandatory = $true)]$AccessToken,[parameter(Mandatory = $true)]$Uri)# Check if authentication was successful.if ($AccessToken) {# Format headers.$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $AccessToken"'ConsistencyLevel' = "eventual" }# Create an empty array to store the result.$QueryResults = @()# Invoke REST method and fetch data until there are no pages left.do {$Results = ""$StatusCode = ""do {try {$Results = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"$StatusCode = $Results.StatusCode} catch {$StatusCode = $_.Exception.Response.StatusCode.value__if ($StatusCode -eq 429) {Write-Warning "Got throttled by Microsoft. Sleeping for 45 seconds..."Start-Sleep -Seconds 45}else {Write-Error $_.Exception}}} while ($StatusCode -eq 429)if ($Results.value) {$QueryResults += $Results.value}else {$QueryResults += $Results}$uri = $Results.'@odata.nextlink'} until (!($uri))# Return the result.$QueryResults}else {Write-Error "No Access Token"}}# Define the values applicable for the application used to connect to the Graph (change these details for your tenant and registered app)$AppSecret = '7RplGSLWoSs~y4uHYy2041-jbm.4~_s.~q'$OutputCSV = "c:\temp\AzureADGuestReviews.CSV"# Construct URI and body needed for authentication$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"$body = @{client_id = $AppIdscope = "https://graph.microsoft.com/.default"client_secret = $AppSecretgrant_type = "client_credentials"}# Get OAuth 2.0 Token$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing# Unpack Access Token$token = ($tokenRequest.Content | ConvertFrom-Json).access_token$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $Token"'ConsistencyLevel' = "eventual" }Write-Host "Fetching Azure AD Access Review Data..."# Get Access Reviews currently running$Uri = "https://graph.microsoft.com/beta/identityGovernance/accessReviews/definitions"$AccessData = Get-GraphData -AccessToken $Token -Uri $uri# Find the access review for Teams and Groups# This check depends on the display name you assign to the access review in the Azure AD admin center$Id = $Accessdata | Where-Object {$_.displayname -eq "Review guest access across Microsoft 365 Groups"} | Select-Object -ExpandProperty IdIf ($Id -eq $Null) { Write-Host "Can't find access review - please check the display name"; break }# Find the instances (groups being reviewed)$Uri = "https://graph.microsoft.com/beta/identityGovernance/accessReviews/definitions/" + $Id +"/instances"$AccessData = Get-GraphData -AccessToken $Token -Uri $uri$CountOfGroups = $AccessData.Count$Report = [System.Collections.Generic.List[Object]]::new()$ApproveCount = 0; $DenyCount = 0; $NoDecision = 0ForEach ($Instance in $AccessData) {$InstanceId = $Instance.Id# Get group id$Start = $Instance.scope.query.IndexOf("s/")$End = $Instance.scope.query.IndexOf("/members")$GroupId = $Instance.scope.query.substring($Start + 2,$End - 13)$Uri = "https://graph.microsoft.com/v1.0/groups/" + $GroupId$GroupDetails = Get-GraphData -AccessToken $Token -Uri $uri$GroupName = $GroupDetails.DisplayName# Now get the instances (people being reviewed) and what's happened to each$GroupUnderReview = 0 # Flag to track if a group has started review. Set if a Deny or Approve decision is made$Uri = "https://graph.microsoft.com/beta/identityGovernance/accessReviews/definitions/" + $Id +"/instances/" + $instanceId + "/decisions"$InstanceData = Get-GraphData -AccessToken $Token -Uri $uriWrite-Host "Number of Guests to review in" $GroupName ":" $InstanceData.id.CountIf ($InstanceData.id.Count -gt 0) {ForEach ($Decision in $InstanceData) {Switch ($Decision.decision) { # Only generate report if guests are present to review"Deny" {$DenyCount++; $GroupUnderReview = 1$Verdict = $Decision.decision$User = $decision.target.userprincipalname$Name = $decision.target.userdisplayname$justification = $decision.justification$Recommendation = $Decision.recommendation$reviewer = $decision.reviewedby.displayname$ReviewDate = (get-date $decision.revieweddatetime -format g) }"Approve" {$ApproveCount++$Verdict = $Decision.decision; $GroupUnderReview = 1$User = $decision.target.userprincipalname$Name = $decision.target.userdisplayname$justification = $decision.justification$Recommendation = $Decision.recommendation$reviewer = $decision.reviewedby.displayname$ReviewDate = (get-date $decision.revieweddatetime -format g) }"NotReviewed" {$NoDecision++$Verdict = $Decision.decision$User = $decision.target.userprincipalname$Name = $decision.target.userdisplayname$justification = $decision.justification$Recommendation = $Decision.recommendation$reviewer = $decision.reviewedby.displayname$ReviewDate = "No decision made" }} #End Switch# Report decision$ReportLine = [PSCustomObject] @{User = $UserName = $NameVerdict = $VerdictRecommendation = $Recommendation.Trim()Reviewer = $ReviewerReviewDate = $ReviewDateJustification = $Justification.Trim()Group = $GroupName }$Report.Add($ReportLine)} # End report if any guests are found to review} #End ForEach InstanceDataIf ($GroupUnderReview -eq 1) { $GroupsWithReview++ }} #End ForEach AccessData# Quick way of reporting counts for the various verdicts is to group the report data, but we want some nice figures# $Report | Group-Object Verdict | format-Table Name, Count$CountOfApprovals = ($Report | Where-Object {$_.Verdict -eq "Approve"} | Measure)$CountOfDeny = ($Report | Where-Object {$_.Verdict -eq "Deny"} | Measure )$CountOfNoDecision = ($Report | Where-Object {$_.Verdict -eq "NotReviewed"} | Measure)Write-Host "Number of Groups with reviews for Guest Members: " $CountOfGroupsCLSWrite-Host ""Write-Host "Decision Profile"Write-Host "----------------"Write-Host ""Write-Host "Total Groups with Guests: " $CountOfGroupsWrite-Host "Groups started reviews: " $GroupsWithReviewWrite-Host "Groups not started reviews " ($CountOfGroups - $GroupsWithReview)Write-Host "Total decisions to be made: " $Report.CountWrite-Host ("Review decisions to approve guest access: {0} ({1})" -f $CountOfApprovals.Count, ($CountOfApprovals.Count/$Report.Count).ToString("P") )Write-Host ("Review decisions to deny guest access: {0} ({1})" -f $CountOfDeny.Count, ($CountOfDeny.Count/$Report.Count).ToString("P") )Write-Host ("No decisions made so far: {0} ({1})" -f $CountOfNoDecision.Count, ($CountOfNoDecision.Count/$Report.Count).ToString("P") )Write-Host " "Write-Host "A CSV file for current Access Review decision status is available in" $OutputCSV# Output files$Report | Sort User | Export-CSV -NoTypeInformation $OutputCSV$Report | Sort User | Out-GridView
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.Attribution
Author
Office365itpros