<# .SYNOPSIS Export all active memberships in privileged Microsoft Entra roles, including expansion of group-assigned roles to transitive group members. .NOTES - Uses Microsoft Graph PowerShell only (no Microsoft.Graph.Beta module import) - Uses Invoke-MgGraphRequest against the beta endpoint only for role definition discovery - Exports CSV to .\Entra-PrivilegedRoleMemberships.csv .REQUIREMENTS Install-Module Microsoft.Graph -Scope CurrentUser .PERMISSIONS Suggested delegated scopes: RoleManagement.Read.Directory Directory.Read.All GroupMember.Read.All #> param( [string]$CsvPath = ".\Entra-PrivilegedRoleMemberships.csv" ) $ErrorActionPreference = "Stop" function Get-ObjectType { param([object]$Object) if ($null -eq $Object.AdditionalProperties.'@odata.type') { return $Object.GetType().Name } return ($Object.AdditionalProperties.'@odata.type' -replace '^#microsoft.graph\.', '') } function Get-DisplayNameSafe { param([object]$Object) if ($Object.PSObject.Properties.Name -contains 'DisplayName' -and $Object.DisplayName) { return $Object.DisplayName } if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('displayName')) { return $Object.AdditionalProperties['displayName'] } return $null } function Get-UpnSafe { param([object]$Object) if ($Object.PSObject.Properties.Name -contains 'UserPrincipalName' -and $Object.UserPrincipalName) { return $Object.UserPrincipalName } if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('userPrincipalName')) { return $Object.AdditionalProperties['userPrincipalName'] } return $null } function Get-MailSafe { param([object]$Object) if ($Object.PSObject.Properties.Name -contains 'Mail' -and $Object.Mail) { return $Object.Mail } if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('mail')) { return $Object.AdditionalProperties['mail'] } return $null } function Invoke-GraphGetAllPages { param( [Parameter(Mandatory)] [string]$Uri ) $items = @() $nextLink = $Uri while ($nextLink) { $response = Invoke-MgGraphRequest -Method GET -Uri $nextLink -OutputType PSObject if ($response.value) { $items += $response.value } else { $items += $response break } $nextLink = $response.'@odata.nextLink' } return $items } Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan Import-Module Microsoft.Graph.Authentication Import-Module Microsoft.Graph.Identity.Governance Import-Module Microsoft.Graph.DirectoryObjects Import-Module Microsoft.Graph.Groups Connect-MgGraph -Scopes @( "RoleManagement.Read.Directory", "Directory.Read.All", "GroupMember.Read.All" ) -NoWelcome Write-Host "Getting privileged role definitions from Graph beta endpoint..." -ForegroundColor Cyan $roleDefsUri = "https://graph.microsoft.com/beta/roleManagement/directory/roleDefinitions?`$filter=isPrivileged eq true" $privilegedRoles = Invoke-GraphGetAllPages -Uri $roleDefsUri if (-not $privilegedRoles) { Write-Warning "No privileged roles were returned." return } Write-Host "Getting active role assignments..." -ForegroundColor Cyan $assignments = Get-MgRoleManagementDirectoryRoleAssignment -All $privilegedRoleIds = @($privilegedRoles | ForEach-Object { $_.id }) $privilegedAssignments = $assignments | Where-Object { $_.RoleDefinitionId -in $privilegedRoleIds } if (-not $privilegedAssignments) { Write-Warning "No active assignments found for privileged roles." return } $roleMap = @{} foreach ($role in $privilegedRoles) { $roleMap[$role.id] = $role.displayName } $principalCache = @{} $groupMemberCache = @{} $results = New-Object System.Collections.Generic.List[object] foreach ($assignment in $privilegedAssignments) { $roleName = $roleMap[$assignment.RoleDefinitionId] $principalId = $assignment.PrincipalId $directoryScopeId = $assignment.DirectoryScopeId if (-not $principalCache.ContainsKey($principalId)) { try { $principalCache[$principalId] = Get-MgDirectoryObjectById -Ids $principalId } catch { Write-Warning "Failed to resolve principal $principalId" continue } } $principal = $principalCache[$principalId] $principalType = Get-ObjectType -Object $principal $principalName = Get-DisplayNameSafe -Object $principal switch ($principalType.ToLower()) { "group" { if (-not $groupMemberCache.ContainsKey($principalId)) { try { $groupMemberCache[$principalId] = Get-MgGroupTransitiveMember -GroupId $principalId -All } catch { Write-Warning "Failed to expand group members for group $principalName ($principalId)" $groupMemberCache[$principalId] = @() } } $members = $groupMemberCache[$principalId] if (-not $members -or $members.Count -eq 0) { $results.Add([pscustomobject]@{ RoleName = $roleName AssignmentType = "GroupAssignment" AssignmentPrincipalType = $principalType AssignmentPrincipal = $principalName AssignmentPrincipalId = $principalId ExpandedMemberType = $null ExpandedMemberName = $null ExpandedMemberUPN = $null ExpandedMemberMail = $null ExpandedMemberId = $null DirectoryScopeId = $directoryScopeId Notes = "Group assigned, but no transitive members returned" }) } else { foreach ($member in $members) { $memberType = Get-ObjectType -Object $member $memberName = Get-DisplayNameSafe -Object $member $memberUpn = Get-UpnSafe -Object $member $memberMail = Get-MailSafe -Object $member $results.Add([pscustomobject]@{ RoleName = $roleName AssignmentType = "GroupAssignmentExpanded" AssignmentPrincipalType = $principalType AssignmentPrincipal = $principalName AssignmentPrincipalId = $principalId ExpandedMemberType = $memberType ExpandedMemberName = $memberName ExpandedMemberUPN = $memberUpn ExpandedMemberMail = $memberMail ExpandedMemberId = $member.Id DirectoryScopeId = $directoryScopeId Notes = "Expanded from group assignment" }) } } } default { $results.Add([pscustomobject]@{ RoleName = $roleName AssignmentType = "DirectAssignment" AssignmentPrincipalType = $principalType AssignmentPrincipal = $principalName AssignmentPrincipalId = $principalId ExpandedMemberType = $principalType ExpandedMemberName = $principalName ExpandedMemberUPN = Get-UpnSafe -Object $principal ExpandedMemberMail = Get-MailSafe -Object $principal ExpandedMemberId = $principalId DirectoryScopeId = $directoryScopeId Notes = "Direct role assignment" }) } } } $results | Sort-Object RoleName, AssignmentType, ExpandedMemberName | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 Write-Host "" Write-Host "Exported to: $CsvPath" -ForegroundColor Green Write-Host "" $results | Sort-Object RoleName, ExpandedMemberName | Format-Table RoleName, AssignmentType, AssignmentPrincipal, ExpandedMemberName, ExpandedMemberUPN, ExpandedMemberType -AutoSize