Files
M365-Scripts/Entra/Export-PrivilegedRolesMembership.ps1

251 lines
8.3 KiB
PowerShell

<#
.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