Skip to content

Commit eb965f9

Browse files
authored
Merge pull request #845 from ValerasNarbutas/newsample/accessReviewresults
Access Review Manager script #844
2 parents 0e83ca9 + 26a1127 commit eb965f9

File tree

4 files changed

+353
-0
lines changed

4 files changed

+353
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# Download Access-Review results across all groups
2+
3+
## Summary
4+
5+
This script automates the management and reporting of Access Reviews in Entra ID. It addresses the common problem where access reviews are often left uncompleted or unattended. The script:
6+
7+
1. Enumerates all access review definitions in your tenant
8+
2. Identifies and triggers any overdue reviews
9+
3. Polls for completion status of active reviews
10+
4. Exports all decisions (accessReviewInstanceDecisionItem) to CSV for audit and record-keeping
11+
12+
The automation helps security and compliance teams ensure that access reviews are completed on time and provides thorough documentation of all decisions made during the review process.
13+
14+
![Example Screenshot](assets/example.png)
15+
16+
## Prerequisites
17+
18+
- Microsoft Graph PowerShell SDK modules
19+
- Permissions required:
20+
- AccessReview.Read.All (to enumerate and read access reviews)
21+
- AccessReview.ReadWrite.All (to start reviews and retrieve decisions)
22+
- An account with appropriate permissions to run access reviews
23+
- PowerShell 5.1 or higher
24+
25+
# [Microsoft Graph PowerShell](#tab/graphps)
26+
27+
```powershell
28+
#-----------------------------------------------------------------------
29+
# Kick-off & Download Access-Review Results Across All Groups
30+
#
31+
# This script will:
32+
# 1. Connect to Microsoft Graph with the required permissions
33+
# 2. Enumerate all access review definitions
34+
# 3. Start any overdue reviews
35+
# 4. Poll for completion
36+
# 5. Export all decisions to CSV for audit purposes
37+
#-----------------------------------------------------------------------
38+
39+
# Check if the required modules are installed and import them
40+
if (!(Get-Module -Name Microsoft.Graph)) {
41+
Write-Host "Microsoft Graph PowerShell module not found. Installing..." -ForegroundColor Yellow
42+
Install-Module Microsoft.Graph -Scope CurrentUser -Force
43+
}
44+
45+
# Connect to Microsoft Graph with the required permissions
46+
Connect-MgGraph -Scopes "AccessReview.Read.All", "AccessReview.ReadWrite.All" -NoWelcome
47+
48+
# Set the output directory for reports
49+
$outputDir = ".\AccessReviewReports"
50+
if (!(Test-Path $outputDir)) {
51+
New-Item -ItemType Directory -Path $outputDir | Out-Null
52+
}
53+
54+
# Get the current date for reporting
55+
$currentDate = Get-Date -Format "yyyy-MM-dd"
56+
$reportFile = Join-Path $outputDir "AccessReviewReport_$currentDate.csv"
57+
$decisionsFile = Join-Path $outputDir "AccessReviewDecisions_$currentDate.csv"
58+
59+
# Function to get all access review definitions
60+
function Get-AllAccessReviews {
61+
try {
62+
# Get all access review definitions
63+
$accessReviews = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions" -Method GET
64+
return $accessReviews.value
65+
}
66+
catch {
67+
Write-Error "Error retrieving access review definitions: $_"
68+
return $null
69+
}
70+
}
71+
72+
# Function to start an overdue access review
73+
function Start-OverdueAccessReview {
74+
param (
75+
[Parameter(Mandatory = $true)]
76+
[string]$ReviewId
77+
)
78+
79+
try {
80+
# Start the access review
81+
Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions/$ReviewId/start" -Method POST
82+
Write-Host "Successfully started access review: $ReviewId" -ForegroundColor Green
83+
return $true
84+
}
85+
catch {
86+
Write-Error "Error starting access review $ReviewId : $_"
87+
return $false
88+
}
89+
}
90+
91+
# Function to get all instances of an access review
92+
function Get-AccessReviewInstances {
93+
param (
94+
[Parameter(Mandatory = $true)]
95+
[string]$ReviewId
96+
)
97+
98+
try {
99+
# Get all instances of the access review
100+
$instances = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions/$ReviewId/instances" -Method GET
101+
return $instances.value
102+
}
103+
catch {
104+
Write-Error "Error retrieving instances for access review $ReviewId : $_"
105+
return $null
106+
}
107+
}
108+
109+
# Function to get all decisions for an access review instance
110+
function Get-AccessReviewDecisions {
111+
param (
112+
[Parameter(Mandatory = $true)]
113+
[string]$ReviewId,
114+
115+
[Parameter(Mandatory = $true)]
116+
[string]$InstanceId
117+
)
118+
119+
try {
120+
# Get all decisions for the access review instance
121+
$decisions = @()
122+
$uri = "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions/$ReviewId/instances/$InstanceId/decisions"
123+
124+
do {
125+
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
126+
$decisions += $response.value
127+
$uri = $response.'@odata.nextLink'
128+
} while ($uri)
129+
130+
return $decisions
131+
}
132+
catch {
133+
Write-Error "Error retrieving decisions for access review instance $InstanceId : $_"
134+
return $null
135+
}
136+
}
137+
138+
# Main script execution
139+
Write-Host "Starting access review management process..." -ForegroundColor Cyan
140+
141+
# Get all access review definitions
142+
$allReviews = Get-AllAccessReviews
143+
if ($null -eq $allReviews) {
144+
Write-Host "No access reviews found or error retrieving them. Exiting." -ForegroundColor Red
145+
exit
146+
}
147+
148+
Write-Host "Found $($allReviews.Count) access review definitions." -ForegroundColor Green
149+
150+
# Create an array to store review information for reporting
151+
$reviewReport = @()
152+
$allDecisions = @()
153+
154+
# Process each access review
155+
foreach ($review in $allReviews) {
156+
Write-Host "Processing Access Review: $($review.displayName)" -ForegroundColor Cyan
157+
158+
# Check if the review is overdue to start
159+
$shouldStart = $false
160+
if ($review.status -eq "NotStarted") {
161+
$startDateTime = [DateTime]::Parse($review.schedule.startDateTime)
162+
if ($startDateTime -lt (Get-Date)) {
163+
$shouldStart = $true
164+
Write-Host " Review is overdue to start. Attempting to start it now..." -ForegroundColor Yellow
165+
$started = Start-OverdueAccessReview -ReviewId $review.id
166+
if ($started) {
167+
$review.status = "InProgress"
168+
}
169+
}
170+
}
171+
172+
# Get instances for this review
173+
$instances = Get-AccessReviewInstances -ReviewId $review.id
174+
if ($null -eq $instances) {
175+
Write-Host " No instances found for this review." -ForegroundColor Yellow
176+
continue
177+
}
178+
179+
Write-Host " Found $($instances.Count) instances for this review." -ForegroundColor Green
180+
181+
# Process each instance
182+
foreach ($instance in $instances) {
183+
# Add to the report
184+
$reviewInfo = [PSCustomObject]@{
185+
ReviewId = $review.id
186+
ReviewName = $review.displayName
187+
InstanceId = $instance.id
188+
Status = $instance.status
189+
StartDateTime = $instance.startDateTime
190+
EndDateTime = $instance.endDateTime
191+
ReviewerCount = $instance.reviewers.Count
192+
WasStartedAutomatically = $shouldStart
193+
}
194+
$reviewReport += $reviewInfo
195+
196+
# If the instance is completed, get all decisions
197+
if ($instance.status -eq "Completed") {
198+
Write-Host " Getting decisions for completed instance $($instance.id)" -ForegroundColor Green
199+
$decisions = Get-AccessReviewDecisions -ReviewId $review.id -InstanceId $instance.id
200+
201+
if ($null -ne $decisions -and $decisions.Count -gt 0) {
202+
foreach ($decision in $decisions) {
203+
$decisionInfo = [PSCustomObject]@{
204+
ReviewId = $review.id
205+
ReviewName = $review.displayName
206+
InstanceId = $instance.id
207+
DecisionId = $decision.id
208+
ResourceId = $decision.resourceId
209+
Decision = $decision.decision
210+
ReviewedBy = $decision.reviewedBy.displayName
211+
ReviewedByUpn = $decision.reviewedBy.userPrincipalName
212+
AppliedBy = $decision.appliedBy.displayName
213+
AppliedDateTime = $decision.appliedDateTime
214+
Justification = $decision.justification
215+
}
216+
$allDecisions += $decisionInfo
217+
}
218+
Write-Host " Retrieved $($decisions.Count) decisions." -ForegroundColor Green
219+
}
220+
else {
221+
Write-Host " No decisions found for this instance." -ForegroundColor Yellow
222+
}
223+
}
224+
}
225+
}
226+
227+
# Export the reports to CSV
228+
if ($reviewReport.Count -gt 0) {
229+
$reviewReport | Export-Csv -Path $reportFile -NoTypeInformation
230+
Write-Host "Exported review report to: $reportFile" -ForegroundColor Green
231+
}
232+
233+
if ($allDecisions.Count -gt 0) {
234+
$allDecisions | Export-Csv -Path $decisionsFile -NoTypeInformation
235+
Write-Host "Exported decision details to: $decisionsFile" -ForegroundColor Green
236+
}
237+
238+
Write-Host "Access review management process completed successfully." -ForegroundColor Cyan
239+
Disconnect-MgGraph
240+
```
241+
[!INCLUDE [More about Microsoft Graph PowerShell SDK](../../docfx/includes/MORE-GRAPHSDK.md)]
242+
***
243+
244+
245+
## Contributors
246+
247+
| Author(s) |
248+
|-----------|
249+
| [Valeras Narbutas](https://github.com/ValerasNarbutas) |
250+
251+
## Additional Information
252+
253+
### Key Microsoft Graph API Endpoints Used
254+
255+
1. List access review definitions:
256+
- `GET /identityGovernance/accessReviews/definitions`
257+
258+
2. Start an access review:
259+
- `POST /identityGovernance/accessReviews/definitions/{reviewId}/start`
260+
261+
3. Get access review instances:
262+
- `GET /identityGovernance/accessReviews/definitions/{reviewId}/instances`
263+
264+
4. Get access review decisions:
265+
- `GET /identityGovernance/accessReviews/definitions/{reviewId}/instances/{instanceId}/decisions`
266+
267+
### Sample Output
268+
269+
The script produces two CSV files:
270+
271+
1. **Access Review Report** - Lists all access review definitions and their instances with status
272+
2. **Access Review Decisions** - Contains all decisions made across completed reviews
273+
274+
### Scheduling the Script
275+
276+
For optimal results, consider scheduling this script to run weekly to ensure that:
277+
- New access reviews are started promptly
278+
- Completed reviews have their decisions exported for record-keeping
279+
- You have visibility into the overall access review health of your tenant
280+
281+
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
282+
<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/aad-access-review-manager" aria-hidden="true" />
Loading
Loading
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
[
2+
{
3+
"name": "aad-access-review-manager",
4+
"source": "pnp",
5+
"title": "Kick-off & download Access-Review results across all groups",
6+
"shortDescription": "This script enumerates all access-review definitions, triggers overdue reviews, polls for completion, and exports decisions to CSV for audit.",
7+
"url": "https://pnp.github.io/script-samples/aad-access-review-manager/README.html",
8+
"longDescription": [
9+
""
10+
],
11+
"creationDateTime": "2025-05-25",
12+
"updateDateTime": "2025-05-25",
13+
"products": [
14+
"SharePoint",
15+
"Office",
16+
"Graph",
17+
"PowerApps",
18+
"Teams",
19+
"Power Automate",
20+
"Microsoft Todo",
21+
"Azure",
22+
"Microsoft 365 Copilot"
23+
],
24+
"metadata": [
25+
{
26+
"key": "GRAPH-POWERSHELL",
27+
"value": "1.0.0"
28+
}
29+
],
30+
"categories": [
31+
"Modernize",
32+
"Data",
33+
"Deploy",
34+
"Provision",
35+
"Configure",
36+
"Report",
37+
"Security",
38+
"AI",
39+
"Microsoft 365 Copilot"
40+
],
41+
"tags": [
42+
"Get-MgIdentityGovernanceAccessReview",
43+
"Start-MgIdentityGovernanceAccessReview",
44+
"Get-MgIdentityGovernanceAccessReviewDecision",
45+
"Export-Csv"
46+
],
47+
"thumbnails": [
48+
{
49+
"type": "image",
50+
"order": 100,
51+
"url": "https://raw.githubusercontent.com/pnp/script-samples/main/scripts/aad-access-review-manager/assets/preview.png",
52+
"alt": "Preview of the sample Kick-off & download Access-Review results across all groups"
53+
}
54+
],
55+
"authors": [
56+
{
57+
"gitHubAccount": "ValerasNarbutas",
58+
"company": "",
59+
"pictureUrl": "https://github.com/ValerasNarbutas.png",
60+
"name": "Valeras Narbutas"
61+
}
62+
],
63+
"references": [
64+
{
65+
"name": "Want to learn more about Microsoft Graph PowerShell SDK and the cmdlets",
66+
"description": "Check out the Microsoft Graph PowerShell SDK documentation site to get started and for the reference to the cmdlets.",
67+
"url": "https://learn.microsoft.com/graph/powershell/get-started"
68+
}
69+
]
70+
}
71+
]

0 commit comments

Comments
 (0)