Azure / Scripts / Governance

Get-AzRoleAssignmentReport

Reports Azure RBAC role assignments in every subscription and flags the risky ones.

Runs in
Az PowerShell
Safety
Read-only

Download Get-AzRoleAssignmentReport.ps1

What it does

Lists every role assignment visible in each subscription (including resource group and resource scope) and flags:

  • Privileged roles: Owner, Contributor, User Access Administrator, Role Based Access Control Administrator
  • Guest (external) accounts
  • Roles granted directly to users rather than groups
  • Assignments whose principal no longer exists (ObjectType Unknown), left behind by deleted accounts

Read-only. Use it for quarterly access reviews.

Parameters

-SubscriptionIdstring[]Limit the report to these subscription IDs. Default: every enabled subscription.
-OutputFilestringCSV output path.Default: Timestamped file in the current folder

Examples

.\Get-AzRoleAssignmentReport.ps1
.\Get-AzRoleAssignmentReport.ps1 -SubscriptionId 00000000-0000-0000-0000-000000000000

Notes

Requires Az.Accounts and Az.Resources, and Reader access (Microsoft.Authorization/roleAssignments/read). Clean up orphaned assignments with Remove-AzRoleAssignment -ObjectId <id> -RoleDefinitionName <role> -Scope <scope>.

Full script

<#
.SYNOPSIS
    Reports Azure RBAC role assignments in every subscription and flags the risky ones.

.DESCRIPTION
    Lists every role assignment visible in each subscription (including resource group
    and resource scope) and flags:
    - Privileged roles: Owner, Contributor, User Access Administrator, Role Based Access Control Administrator
    - Guest (external) accounts
    - Roles granted directly to users rather than groups
    - Assignments whose principal no longer exists (ObjectType Unknown), left behind by deleted accounts

    Read-only. Use it for quarterly access reviews.

.PARAMETER SubscriptionId
    Limit the report to these subscription IDs. Default: every enabled subscription.

.PARAMETER OutputFile
    CSV output path.

.EXAMPLE
    .\Get-AzRoleAssignmentReport.ps1

.EXAMPLE
    .\Get-AzRoleAssignmentReport.ps1 -SubscriptionId 00000000-0000-0000-0000-000000000000

.NOTES
    Requires Az.Accounts and Az.Resources, and Reader access (Microsoft.Authorization/roleAssignments/read).
    Clean up orphaned assignments with Remove-AzRoleAssignment -ObjectId <id> -RoleDefinitionName <role> -Scope <scope>.
#>
[CmdletBinding()]
param(
    [string[]]$SubscriptionId,
    [string]$OutputFile = (Join-Path -Path (Get-Location) -ChildPath ('AzRoleAssignments_{0:yyyyMMdd_HHmm}.csv' -f (Get-Date)))
)

if (-not (Get-AzContext)) { throw 'Not signed in. Run Connect-AzAccount first.' }

$privileged = @('Owner', 'Contributor', 'User Access Administrator', 'Role Based Access Control Administrator')
$subs = @(Get-AzSubscription | Where-Object { $_.State -eq 'Enabled' })
if ($SubscriptionId) { $subs = @($subs | Where-Object { $SubscriptionId -contains $_.Id }) }

$report = New-Object System.Collections.Generic.List[object]
foreach ($sub in $subs) {
    Write-Host "Reading $($sub.Name)..." -ForegroundColor Cyan
    Set-AzContext -Subscription $sub.Id | Out-Null
    foreach ($ra in Get-AzRoleAssignment) {
        $flags = New-Object System.Collections.Generic.List[string]
        if ($privileged -contains $ra.RoleDefinitionName) { $flags.Add('Privileged role') }
        if ($ra.ObjectType -eq 'Unknown') { $flags.Add('Principal deleted') }
        if ($ra.SignInName -like '*#EXT#*') { $flags.Add('Guest') }
        if ($ra.ObjectType -eq 'User') { $flags.Add('Direct user assignment') }

        $scopeLevel = switch -Regex ($ra.Scope) {
            '^/providers/Microsoft.Management/managementGroups/' { 'Management group (inherited)'; break }
            '^/$'                                                { 'Root (inherited)'; break }
            '^/subscriptions/[^/]+$'                             { 'Subscription'; break }
            '^/subscriptions/[^/]+/resourceGroups/[^/]+$'        { 'Resource group'; break }
            default                                              { 'Resource' }
        }

        $report.Add([pscustomobject]@{
            Subscription = $sub.Name
            ScopeLevel   = $scopeLevel
            Scope        = $ra.Scope
            Role         = $ra.RoleDefinitionName
            Principal    = $ra.DisplayName
            SignInName   = $ra.SignInName
            ObjectType   = $ra.ObjectType
            ObjectId     = $ra.ObjectId
            Flags        = $flags -join '; '
        })
    }
}

$report | Sort-Object Subscription, ScopeLevel, Role | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8

Write-Host "Assignments: $($report.Count)" -ForegroundColor Cyan
[pscustomobject]@{
    PrivilegedRoles         = @($report | Where-Object { $_.Flags -like '*Privileged role*' }).Count
    PrivilegedDirectToUsers = @($report | Where-Object { $_.Flags -like '*Privileged role*' -and $_.ObjectType -eq 'User' }).Count
    Guests                  = @($report | Where-Object { $_.Flags -like '*Guest*' }).Count
    DeletedPrincipals       = @($report | Where-Object { $_.Flags -like '*Principal deleted*' }).Count
} | Format-List | Out-Host
Write-Host "Report: $OutputFile" -ForegroundColor Green