Report Expiring Admin Role Assignments (PIM) Using Graph PowerShell

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:

  • critical admins losing access during incidents
  • service owners forgetting to renew elevated roles
  • audit findings due to inconsistent role governance
  • last-minute emergencies when an assignment expires unexpectedly

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.

πŸš€ Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool β€” your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.

i) The Script

$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
                                

ii) How the Script Works

  1. Sets sender, recipients, and expiry window
    You define the sender mailbox, stakeholder emails, and how many days ahead to check (default: 30).
  2. Connects to Microsoft Graph with PIM read scopes
    The script uses least-privileged PIM-related permissions such as:
    • RoleAssignmentSchedule.Read.Directory
    • RoleManagement.Read.Directory
      These are required to read PIM role assignment schedules and instances.
  3. Pulls active PIM assignment schedule instances
  4. Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance returns active admin assignments, including start/end times.

  5. Filters assignments expiring soon
    Any assignment whose EndDateTime is less than or equal to the cutoff gets included.
  6. Resolves role and user details
    • Role name comes from Get-MgRoleManagementDirectoryRoleDefinition
    • User details come from Get-MgUser
      This makes the final report human-readable.
  7. Exports an Excel-safe CSV
    UTF-8 export + BOM rewrite ensures Excel always shows columns correctly β€” even when no results are found (your preferred pattern).
  8. Emails the report using Graph
    The CSV is Base64-encoded and attached to a Graph email payload sent via Send-MgUserMail (no SMTP config needed).

iii) Further Enhancements

  • Include eligible assignments too
    Eligible admins can activate roles on demand. You can add Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance to report upcoming eligible expiries.
  • Add role activation status context
    Show whether the admin is currently active or time-bound via activation.
  • Alert tiers
    • expires in 7 days β†’ High priority
    • expires in 30 days β†’ Medium priority
  • Automatically notify role owners
    Resolve admin managers or app owners and email them directly.
  • Schedule weekly governance checks
    Run using Task Scheduler or Azure Automation to keep PIM hygiene consistent.

iv) Possible Errors & Solutions

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.


v) Conclusion

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.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex