Control 4.2: Site Access Reviews and Certification - PowerShell Setup
This playbook provides PowerShell automation guidance for Control 4.2.
Prerequisites
# Install required modules
Install-Module Microsoft.Graph -Scope CurrentUser
Install-Module Microsoft.Online.SharePoint.PowerShell -Scope CurrentUser
# Connect to Microsoft Graph with required scopes
Connect-MgGraph -Scopes "AccessReview.ReadWrite.All", "Directory.Read.All", "Sites.Read.All"
# Connect to SharePoint Online
$adminUrl = "https://yourtenant-admin.sharepoint.com"
Connect-SPOService -Url $adminUrl
# Verify connections
Get-MgContext | Select-Object Scopes, Account
Get-SPOTenant | Select-Object StorageQuota
Configuration Scripts
Create Access Review Schedule for SharePoint Site Groups
# Define the access review schedule definition
$reviewParams = @{
displayName = "FSI SharePoint Site Access Review - Enterprise Managed"
descriptionForAdmins = "Quarterly access review for enterprise-managed SharePoint sites containing agent knowledge sources"
descriptionForReviewers = "Review and certify that users have appropriate access to sensitive SharePoint sites"
scope = @{
"@odata.type" = "#microsoft.graph.accessReviewQueryScope"
query = "/groups?`$filter=(groupTypes/any(c:c eq 'Unified'))"
queryType = "MicrosoftGraph"
}
reviewers = @(
@{
query = "/groups/{group-id}/owners"
queryType = "MicrosoftGraph"
}
)
settings = @{
mailNotificationsEnabled = $true
reminderNotificationsEnabled = $true
justificationRequiredOnApproval = $true
defaultDecisionEnabled = $true
defaultDecision = "Deny"
instanceDurationInDays = 14
autoApplyDecisionsEnabled = $true
recommendationsEnabled = $true
recurrence = @{
pattern = @{
type = "absoluteMonthly"
interval = 3 # Quarterly
dayOfMonth = 1
}
range = @{
type = "noEnd"
startDate = (Get-Date).ToString("yyyy-MM-dd")
}
}
}
}
# Create the access review schedule definition
$review = New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $reviewParams
Write-Host "Access Review Created: $($review.DisplayName)" -ForegroundColor Green
Write-Host "Review ID: $($review.Id)" -ForegroundColor Cyan
Get Access Review Status
# Get all access review definitions
$accessReviews = Get-MgIdentityGovernanceAccessReviewDefinition
$accessReviews | Format-Table DisplayName, Status, CreatedDateTime
# Get specific access review instances
$reviewId = "your-review-definition-id"
$instances = Get-MgIdentityGovernanceAccessReviewDefinitionInstance -AccessReviewScheduleDefinitionId $reviewId
# Display instance details
$instances | ForEach-Object {
Write-Host "Instance: $($_.Id)" -ForegroundColor Yellow
Write-Host " Status: $($_.Status)"
Write-Host " Start: $($_.StartDateTime)"
Write-Host " End: $($_.EndDateTime)"
Write-Host " Reviewers Completed: $($_.ReviewersCompleted) / $($_.ReviewersTotal)"
}
# Get pending decisions for an instance
$instanceId = "your-instance-id"
$decisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision `
-AccessReviewScheduleDefinitionId $reviewId `
-AccessReviewInstanceId $instanceId
$decisions | Where-Object { $_.Decision -eq "NotReviewed" } |
Format-Table Principal, Resource, Decision, ReviewedDateTime
Export Access Review Results
# Export access review decisions to CSV for audit
$reviewId = "your-review-definition-id"
$instances = Get-MgIdentityGovernanceAccessReviewDefinitionInstance `
-AccessReviewScheduleDefinitionId $reviewId
$allDecisions = @()
foreach ($instance in $instances) {
$decisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision `
-AccessReviewScheduleDefinitionId $reviewId `
-AccessReviewInstanceId $instance.Id `
-All
foreach ($decision in $decisions) {
$allDecisions += [PSCustomObject]@{
InstanceId = $instance.Id
InstanceStartDate = $instance.StartDateTime
PrincipalName = $decision.Principal.DisplayName
PrincipalType = $decision.Principal.Type
ResourceName = $decision.Resource.DisplayName
Decision = $decision.Decision
Justification = $decision.Justification
ReviewedBy = $decision.ReviewedBy.DisplayName
ReviewedDateTime = $decision.ReviewedDateTime
AppliedBy = $decision.AppliedBy.DisplayName
AppliedDateTime = $decision.AppliedDateTime
}
}
}
# Export to CSV
$exportPath = "C:\Compliance\AccessReview_Export_$(Get-Date -Format 'yyyyMMdd').csv"
$allDecisions | Export-Csv -Path $exportPath -NoTypeInformation
Write-Host "Exported $($allDecisions.Count) decisions to: $exportPath" -ForegroundColor Green
Get SharePoint Site Permissions Report
# Get sites with sharing settings
$sites = Get-SPOSite -Limit All | Where-Object {
$_.SharingCapability -ne "Disabled" -and
$_.Template -notlike "*SPSPERS*" # Exclude OneDrive
}
# Generate permissions summary
$siteSummary = $sites | ForEach-Object {
[PSCustomObject]@{
SiteUrl = $_.Url
Title = $_.Title
Owner = $_.Owner
SharingCapability = $_.SharingCapability
ExternalSharingEnabled = $_.SharingCapability -ne "Disabled"
SensitivityLabel = $_.SensitivityLabel
LastContentModified = $_.LastContentModifiedDate
}
}
# Export for review
$siteSummary | Export-Csv -Path "C:\Compliance\SPOSitePermissions_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host "Site permissions report exported" -ForegroundColor Green
Audit Agent Service Principal Permissions
function Get-AgentServicePrincipalPermissions {
param([string]$AppId)
Connect-MgGraph -Scopes "Application.Read.All", "Directory.Read.All"
# Get service principal
$sp = Get-MgServicePrincipal -Filter "appId eq '$AppId'"
if (-not $sp) {
Write-Error "Service principal not found for AppId: $AppId"
return
}
Write-Host "Service Principal: $($sp.DisplayName)" -ForegroundColor Cyan
Write-Host "App ID: $AppId"
# Get OAuth2 permission grants (delegated permissions)
$delegated = Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $sp.Id
Write-Host "`nDelegated Permissions:" -ForegroundColor Yellow
$delegated | ForEach-Object {
$resource = Get-MgServicePrincipal -ServicePrincipalId $_.ResourceId
Write-Host " $($resource.DisplayName): $($_.Scope)"
}
# Get app role assignments (application permissions)
$appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
Write-Host "`nApplication Permissions:" -ForegroundColor Yellow
foreach ($role in $appRoles) {
$resource = Get-MgServicePrincipal -ServicePrincipalId $role.ResourceId
$roleInfo = $resource.AppRoles | Where-Object { $_.Id -eq $role.AppRoleId }
Write-Host " $($resource.DisplayName): $($roleInfo.Value)"
}
# Evaluate least privilege
Write-Host "`nLeast Privilege Assessment:" -ForegroundColor Green
# Check for overly broad SharePoint permissions
$broadPermissions = @("Sites.Read.All", "Sites.ReadWrite.All", "Sites.Manage.All", "Sites.FullControl.All")
foreach ($role in $appRoles) {
$resource = Get-MgServicePrincipal -ServicePrincipalId $role.ResourceId
$roleInfo = $resource.AppRoles | Where-Object { $_.Id -eq $role.AppRoleId }
if ($roleInfo.Value -in $broadPermissions) {
Write-Host " WARNING: Broad permission detected - $($roleInfo.Value)" -ForegroundColor Red
Write-Host " Consider using Sites.Selected for specific site access" -ForegroundColor Yellow
}
}
}
# Usage:
# Get-AgentServicePrincipalPermissions -AppId "your-app-id-here"
Complete Configuration Script
<#
.SYNOPSIS
Configures Control 4.2 - Site Access Reviews and Certification
.DESCRIPTION
This script creates access review schedules and exports site permission reports:
1. Creates quarterly access review for SharePoint site groups
2. Exports site permissions report for review
3. Audits agent service principal permissions
.PARAMETER TenantAdminUrl
The SharePoint Admin Center URL
.PARAMETER CreateAccessReview
Switch to create new access review schedule
.EXAMPLE
.\Configure-Control-4.2.ps1 -TenantAdminUrl "https://contoso-admin.sharepoint.com"
.NOTES
Last Updated: January 2026
Related Control: Control 4.2 - Site Access Reviews and Certification
#>
param(
[Parameter(Mandatory=$true)]
[string]$TenantAdminUrl,
[switch]$CreateAccessReview
)
try {
# Connect to services
Write-Host "Connecting to services..." -ForegroundColor Cyan
Connect-MgGraph -Scopes "AccessReview.ReadWrite.All", "Directory.Read.All"
Connect-SPOService -Url $TenantAdminUrl
# Export site permissions
Write-Host "`nExporting site permissions..." -ForegroundColor Yellow
$sites = Get-SPOSite -Limit All | Where-Object { $_.Template -notlike "*SPSPERS*" }
$sites | Select-Object Url, Title, Owner, SharingCapability, SensitivityLabel |
Export-Csv -Path "SitePermissions_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host " [DONE] Exported $($sites.Count) sites" -ForegroundColor Green
# List existing access reviews
Write-Host "`nExisting Access Reviews:" -ForegroundColor Yellow
Get-MgIdentityGovernanceAccessReviewDefinition | Format-Table DisplayName, Status
if ($CreateAccessReview) {
Write-Host "`nCreating new access review..." -ForegroundColor Yellow
# Add review creation logic here
Write-Host " [INFO] Use the detailed script above to create access reviews" -ForegroundColor Cyan
}
Write-Host "`nControl 4.2 setup complete!" -ForegroundColor Green
Write-Host "`n[PASS] Control 4.2 configuration completed successfully" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "[INFO] Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Yellow
exit 1
}
finally {
# Cleanup connections
Disconnect-MgGraph -ErrorAction SilentlyContinue
if (Get-SPOSite -Limit 1 -ErrorAction SilentlyContinue) {
Disconnect-SPOService -ErrorAction SilentlyContinue
}
}
Back to Control 4.2 | Portal Walkthrough | Verification Testing | Troubleshooting
Updated: January 2026 | Version: v1.2