During an Azure governance review last month, a customer asked a simple question. Who has Owner role assignments in their tenant, and at which level of the management group hierarchy? The Azure portal answered the first half, then asked us to click into every Management Group and Subscription one by one to finish the answer.
If you have a flat tenant with three subscriptions, that is fine. If you have a Tenant Root Group with five top level MGs, eighteen child MGs, forty subscriptions, and an “everything” group at the root that someone added a person to in 2022, the portal stops being a useful answer.
This is what the script ships in two CSVs.
What it collects
Starting from the Tenant Root Group, or from a Management Group or Subscription you pick, the script traverses recursively (with -Recurse) and runs Get-AzRoleAssignment against every MG and subscription it finds. For each assignment it pulls:
- The principal, resolved to a friendly name (User to UPN, Group to display name, Service Principal to app name)
- The role definition (built-in or custom, with its Actions and DataActions if you want them)
- The scope, broken into ManagementGroup, Subscription, ResourceGroup, or Resource
- For ResourceGroup and Resource scopes, the tags on the underlying object
Tags matter. A role assignment on a resource group called rg-prod-payments with costcenter=4711 and data-classification=pii carries a different weight than the same role on rg-sandbox-test with no tags. Without tags in the CSV, you cannot triage.
Two CSVs
role_assignments.csv is one row per assignment. Columns I open first:
FriendlyNameandPrincipalName, so you do not paste GUIDs into a meetingRoleNameandScopeTypeResourceName,ResourceType,ResourceGroup,SubscriptionNameTags
roles_in_use.csv is one row per unique role definition assigned anywhere in your scope. Columns I open first:
RoleNameandIsCustomAssignments(count of how many places the role is assigned)ScopesCountandScopeTypesUsedActions,DataActions,NotActions,NotDataActions
The second CSV is a fast read of “what RBAC does our tenant actually rely on, and which custom roles do we own.” Custom roles often outlive their original purpose, and aggregating them like this surfaces the ones nobody is using.
Typical usage
# Recursive scan from Tenant Root Group, output to ./output
./Export-AzureRbacAssignments.ps1 -Recurse
# Add JSON output alongside CSV
./Export-AzureRbacAssignments.ps1 -Recurse -OutputJson
# Include Classic Administrators (legacy co-admins still present in some tenants)
./Export-AzureRbacAssignments.ps1 -Recurse -IncludeClassicAdmins
The interactive scope menu lists Tenant Root Group, every MG you can see, and every subscription you can see. Pick comma-separated keys (TRG, MG3, S1), or hit enter to default to TRG.
On a tenant with two MGs and twelve subscriptions, a recursive scan finishes in under three minutes. Most of the time is spent enumerating role assignments at the resource level inside each subscription. Scope it down to MG level only and the script returns in seconds.
What I look for first
Three pivots cover the bulk of governance value:
- Filter
RoleName = Ownerand group byScopeType. Owner at MG level is rarely the right answer. Owner at subscription level needs a written justification. Owner at resource group or resource level is usually a project that ended without cleanup. - Filter
IsCustom = TRUEin roles_in_use.csv. Custom roles with a single assignment, or with zero data actions, or with actions that overlap fully with a built-in role, are candidates to delete. - Cross-reference
PrincipalType = ServicePrincipalwith the App Registrations export from my previous post. A service principal that holds broad Azure RBAC AND broad Microsoft Graph grants is a high value target for an attacker.
Together with the access review automation and PIM activation work, this completes a small mini-series. Know what you have before you make decisions about it.
Where to get it
The script lives on GitHub as part of my EntraGate work.
Source and latest version: maskovli/entragate, Export-AzureRbacAssignments.ps1
Pull requests welcome. If you find a tenant shape that breaks recursion (deep MG trees with restricted permissions, classic deployment models still in production), open an issue.