251 lines
8.3 KiB
PowerShell
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 |