Azure PIM & Role Assignment Audit Script

azure powershell security pim entra-id identity

Managing role assignments and Privileged Identity Management (PIM) across a large Azure environment quickly becomes opaque — especially when you have 10+ subscriptions, an ALZ hierarchy, and a mix of legacy static assignments alongside newer PIM eligible setups. This post walks through a PowerShell script I built to get a full picture of who has what access, and where the cleanup backlog is.

The Problem

In a mature Azure tenant you typically end up with:

  • Static role assignments that were added years ago and never reviewed — Owner and Contributor on subscriptions, Global Administrator without expiry, service accounts with permanent access no one remembers why they have
  • PIM eligible assignments that are configured correctly but hard to document and report on
  • Deleted or external principals — role assignments pointing at accounts that no longer exist
  • Role-assignable groups where membership is opaque and access is inherited transitively

Getting a clear answer to "who has what" requires querying multiple APIs across Entra ID, Azure Resource Manager, and potentially Management Groups — and then correlating the results. That's what this script does.


What the Script Does

Get-AzurePIMReport.ps1 performs a full audit sweep across a single tenant and produces two output files: an interactive HTML report and a CSV for further processing.

Data collected:

  • Entra ID static directory role assignments (via Microsoft Graph)
  • Entra ID PIM eligible assignments (via Microsoft Graph)
  • Azure Resource static role assignments across all active subscriptions (via Get-AzRoleAssignment)
  • Azure Resource PIM eligible assignments — where accessible (via ARM REST API)
  • Management Group scope assignments (optional, via ARM REST API)

For each assignment the script resolves:

  • Principal display name and UPN via directoryObjects/{id} — a single Graph call that returns the type and name regardless of whether the object is a user, group, or service principal
  • Transitive group members — who is actually inside each group, including nested groups
  • Role definition name — resolved from the role definition ID
  • Risk score — HIGH, MEDIUM, LOW, or OK based on role privilege and assignment type

Risk Scoring

The script categorises every assignment into one of four risk levels:

Level Condition
🔴 HIGH Static assignment on a privileged role (Owner, Contributor, Global Administrator, etc.)
🟠 MEDIUM Static assignment on any other role — candidate for conversion to PIM
🟢 LOW PIM eligible on a privileged role — correct setup, worth documenting
OK PIM eligible on a standard role

The HIGH list is your immediate cleanup backlog. The MEDIUM list is your longer-term PIM conversion backlog.


The HTML Report

The output HTML file is fully self-contained — no server, no dependencies, just open it in a browser. All filtering and sorting happens client-side in JavaScript.

Filters available:

  • Assignment type — toggle Static and PIM Eligible independently
  • Principal type — User, Group, App/Service Principal, Managed Identity, Deleted/External
  • Risk level — click the summary cards or use the chips
  • Source — Entra ID, Azure Resource, or Management Group
  • Free-text search — across name, UPN, role, scope, and group members
  • Column sorting — click any header

Hovering a truncated cell shows the full text as a tooltip — useful for long scope paths and group member lists.


Requirements

PowerShell 7 (not compatible with Windows PowerShell 5.1 / ISE due to use of null-coalescing ?? syntax).

Modules:

Install-Module Az.Accounts    -Scope CurrentUser
Install-Module Az.Resources   -Scope CurrentUser
Install-Module Microsoft.Graph -Scope CurrentUser

Permissions required:

Scope Permission
Entra ID Privileged Role Administrator or Global Reader
Microsoft Graph RoleManagement.Read.All, PrivilegedAccess.Read.AzureAD, Directory.Read.All
Azure Subscriptions Reader on all subscriptions
Management Groups (optional) Reader on all management groups

The script requests the Graph scopes automatically — a browser login window will appear.


Usage

# Standard run — two browser logins (Az + Graph)
.\Get-AzurePIMReport.ps1 -TenantId "contoso.onmicrosoft.com"

# Include Management Group scope
.\Get-AzurePIMReport.ps1 -TenantId "contoso.onmicrosoft.com" -IncludeManagementGroups

# Custom output path
.\Get-AzurePIMReport.ps1 -TenantId "contoso.onmicrosoft.com" -OutputPath "C:\Audit\PIM_March2026"

# Skip group member expansion (faster on large tenants)
.\Get-AzurePIMReport.ps1 -TenantId "contoso.onmicrosoft.com" -SkipGroupExpansion

The -TenantId parameter is mandatory — this ensures you always log in to the correct tenant and never accidentally audit the wrong one. Each run performs a fresh login and clears the token cache.


Known Limitations

PIM eligible assignments on subscriptions may return 401. The roleEligibilityScheduleInstances REST endpoint requires an active ARM role on the subscription. If your account only has an eligible (not yet activated) PIM assignment, the call is rejected. The script logs this as [WARN] and continues — static assignments are always collected successfully. To collect PIM eligible data, activate your PIM role before running, or use a service principal with a permanent Reader role.

Role-assignable group members are not enumerable. Groups with isAssignableToRole = true — the groups used to assign PIM roles — block the transitiveMembers endpoint by Microsoft design. This is an intentional security measure. The script detects these automatically and labels them [role-assignable group: member list restricted by Microsoft] rather than throwing an error.

Deleted or cross-tenant principals show as Deleted/External. If a role assignment references an account that has been deleted, or a service principal from another tenant, it cannot be resolved to a display name. These are surfaced in the report with type Deleted/External and are good candidates for immediate cleanup — the principal no longer exists but the assignment is still consuming a role slot.


Download

The zip contains:

  • Get-AzurePIMReport.ps1 — the audit script
  • Get-AzurePIMReport.md — full documentation (also suitable as a repo README)

Download Get-AzurePIMReport.zip{.button .button-primary}


Cleanup Workflow

Once you have the report, a practical approach:

  1. Start with HIGH — for each entry, decide: convert to PIM eligible, or remove entirely. Prioritise Global Administrator and Owner assignments with no expiry.
  2. Work through Deleted/External — these can almost always be removed immediately. The principal is gone; the assignment is an orphan.
  3. Tackle MEDIUM — identify static assignments that should be PIM eligible. Create the eligible assignment, verify the user can activate it, then remove the static one.
  4. Document LOW and OK — these are your healthy PIM assignments. Use the report as a baseline for future access reviews.

Re-run the script after each cleanup round to track progress.

Previous Post Next Post