Privileged Identity Management (PIM) helps enforce least privilege by making admin role assignments time-bound. Thatβs fantastic for security β but it creates an operational risk too: role assignments can expire quietly, leaving admins without access when they need it most.
Common real-world pain points include:
This script proactively detects active Entra admin role assignments expiring within the next 30 days, generates a report, and emails it automatically to administrators or security stakeholders. It relies on the Graph PIM role assignment schedule instances endpoint, which represents time-bound/admin assignments.
Try the M365Corner Microsoft 365 Reporting Tool β your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.
$SenderUPN = "admin@yourtenant.onmicrosoft.com"
$Recipients = @(
"admin@yourtenant.onmicrosoft.com",
"securityteam@yourtenant.onmicrosoft.com"
)
$DaysToExpire = 30
$CutoffDate = (Get-Date).ToUniversalTime().AddDays($DaysToExpire)
Connect-MgGraph -Scopes "RoleAssignmentSchedule.Read.Directory","RoleManagement.Read.Directory","Directory.Read.All","User.Read.All","Mail.Send"
$Schedules = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All `
-Property Id,PrincipalId,RoleDefinitionId,StartDateTime,EndDateTime
$Report = @()
foreach ($s in $Schedules) {
if (-not $s.EndDateTime) { continue }
$EndUtc = ([datetime]$s.EndDateTime).ToUniversalTime()
if ($EndUtc -le $CutoffDate) {
$DaysLeft = [int](($EndUtc - (Get-Date).ToUniversalTime()).TotalDays)
$RoleDef = $null
$UserObj = $null
try {
$RoleDef = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $s.RoleDefinitionId `
-Property DisplayName
} catch {}
try {
$UserObj = Get-MgUser -UserId $s.PrincipalId -Property DisplayName,UserPrincipalName,Mail
} catch {}
$Report += [PSCustomObject]@{
"Admin Name" = if ($UserObj) { $UserObj.DisplayName } else { $s.PrincipalId }
"User Principal Name"= if ($UserObj) { $UserObj.UserPrincipalName } else { "N/A" }
"Admin Email" = if ($UserObj) { $UserObj.Mail } else { "N/A" }
"Admin Role" = if ($RoleDef) { $RoleDef.DisplayName } else { $s.RoleDefinitionId }
"Assignment Start" = $s.StartDateTime
"Assignment End" = $s.EndDateTime
"Days Remaining" = $DaysLeft
"Schedule Instance Id"= $s.Id
}
}
}
$ReportPath = "$env:TEMP\Expiring_Admin_Role_Assignments.csv"
if ($Report.Count -gt 0) {
$Report | Sort-Object "Days Remaining","Admin Role","Admin Name" |
Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
} else {
"No admin role assignments expiring within $DaysToExpire days were found." |
Set-Content -Path $ReportPath -Encoding utf8
}
$Bytes = [System.IO.File]::ReadAllBytes($ReportPath)
$Utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($ReportPath, [System.Text.Encoding]::UTF8.GetString($Bytes), $Utf8Bom)
$Count = $Report.Count
$Subject = "Expiring Admin Role Assignments (Next $DaysToExpire Days) β $(Get-Date -Format 'yyyy-MM-dd')"
$Body = @"
Hello Team,<br><br>
Attached is the <b>Expiring Admin Role Assignments</b> report.<br>
This includes active Entra admin role assignments that expire within the next <b>$DaysToExpire days</b>.<br><br>
Total expiring assignments found: <b>$Count</b><br><br>
Regards,<br>
Graph PowerShell Automation
"@
$AttachmentContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($ReportPath))
$Attachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "Expiring_Admin_Role_Assignments.csv"
ContentBytes = $AttachmentContent
}
)
$ToRecipients = $Recipients | ForEach-Object {
@{ EmailAddress = @{ Address = $_ } }
}
$Message = @{
Message = @{
Subject = $Subject
Body = @{
ContentType = "HTML"
Content = $Body
}
ToRecipients = $ToRecipients
Attachments = $Attachments
}
SaveToSentItems = "true"
}
Send-MgUserMail -UserId $SenderUPN -BodyParameter $Message
Write-Host "Expiring admin role assignments report emailed successfully." -ForegroundColor Green
Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance returns active admin assignments, including start/end times.
| Error | Cause | Solution |
|---|---|---|
| 403 Access Denied / Insufficient privileges | Missing PIM scopes or admin consent. | Ensure the session has one of the supported permissions like RoleAssignmentSchedule.Read.Directory or RoleManagement.Read.Directory. |
| Empty report even though PIM is in use | No assignments expiring within your $DaysToExpire window. | Reduce window to test, e.g., 7 days, or expand to 60/90 days. |
| RoleDefinition/User lookup fails for some entries | Some schedule principals may be groups/service principals or deleted accounts. | Script already handles this by falling back to IDs. |
| Throttling in large tenants | Lots of schedules + per-item lookups. | Re-run later or add lightweight retry/backoff. |
Time-bound admin assignments are the heart of PIM β but they must be monitored, or they can create silent access outages. This script provides a proactive, automated way to identify expiring admin role assignments, generate an audit-ready Excel report, and email stakeholders without SMTP overhead.
Run it regularly to avoid surprise privilege loss and to keep privileged access governance transparent and predictable.
© m365corner.com. All Rights Reserved. Design by HTML Codex