Requirements
Microsoft.Graph.Authentication; delegated sign-in with required consent; appropriate tenant service availability.
Permissions
Directory.Read.All, User.Read.All
PowerShell source / read-only remote queries
Report activated directory roles and their members.
Microsoft.Graph.Authentication; delegated sign-in with required consent; appropriate tenant service availability.
Directory.Read.All, User.Read.All
Local CSV reports and console inventory. Tenant reads are read-only; creating report directories and files writes to the local filesystem.
Review against your environment before execution. Reports may contain personal or tenant identifiers. Do not publish live exports. This page does not run commands.
<#
.SYNOPSIS
Reports activated Microsoft Entra directory roles and their members.
#>
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path $PSScriptRoot -Parent
$ReportsPath = Join-Path $ProjectRoot "reports"
if (-not (Test-Path $ReportsPath)) { New-Item -ItemType Directory -Path $ReportsPath -Force | Out-Null }
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
$RequiredScopes = @("Directory.Read.All","User.Read.All")
$Context = Get-MgContext
if (-not $Context) {
Connect-MgGraph -Scopes $RequiredScopes -UseDeviceCode -NoWelcome
$Context = Get-MgContext
}
if (-not $Context) { throw "Microsoft Graph authentication could not be established." }
function Get-GraphCollection {
param([Parameter(Mandatory=$true)][string]$Uri)
$Results=@(); $NextUri=$Uri
while($NextUri){
$Response=Invoke-MgGraphRequest -Method GET -Uri $NextUri
if($Response["value"]){$Results+=@($Response["value"])}
$NextUri=$Response["@odata.nextLink"]
}
return $Results
}
$Roles = @(Get-GraphCollection -Uri "https://graph.microsoft.com/v1.0/directoryRoles?`$select=id,displayName,description,roleTemplateId")
$RoleInventory=@()
$FailedRoleQueries=@()
foreach($Role in $Roles){
$RoleId=$Role["id"]
try{
# The directoryRoles/{id}/members endpoint was validated without $top.
$Members=@(Get-GraphCollection -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$RoleId/members")
if($Members.Count -eq 0){
$RoleInventory += [PSCustomObject]@{
RoleDisplayName=$Role["displayName"]; RoleDescription=$Role["description"]
RoleTemplateId=$Role["roleTemplateId"]; RoleId=$RoleId
MemberDisplayName=""; MemberUPN=""; MemberMail=""; MemberType=""; MemberId=""
}
continue
}
foreach($Member in $Members){
$RoleInventory += [PSCustomObject]@{
RoleDisplayName=$Role["displayName"]; RoleDescription=$Role["description"]
RoleTemplateId=$Role["roleTemplateId"]; RoleId=$RoleId
MemberDisplayName=$Member["displayName"]; MemberUPN=$Member["userPrincipalName"]
MemberMail=$Member["mail"]; MemberType=$Member["@odata.type"]; MemberId=$Member["id"]
}
}
} catch {
$FailedRoleQueries += [PSCustomObject]@{RoleDisplayName=$Role["displayName"];RoleId=$RoleId;Error=$_.Exception.Message}
}
}
if($FailedRoleQueries.Count -gt 0){ throw "$($FailedRoleQueries.Count) role member query or queries failed." }
$ReportPath=Join-Path $ReportsPath "Entra-Role-Inventory.csv"
$RoleInventory | Sort-Object RoleDisplayName,MemberDisplayName | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8
Write-Host "Activated Roles: $($Roles.Count)"
Write-Host "Roles With Members: $(@($RoleInventory | Where-Object MemberId -ne '' | Select-Object -ExpandProperty RoleId -Unique).Count)"
Write-Host "Roles Without Members: $(@($RoleInventory | Where-Object MemberId -eq '' | Select-Object -ExpandProperty RoleId -Unique).Count)"
Write-Host "Role Membership Rows: $(@($RoleInventory | Where-Object MemberId -ne '').Count)"
Write-Host "Report exported to: $ReportPath" -ForegroundColor Green