Public/Exchange/Role/Get-PurviewRoleReport.ps1
|
<#
.SYNOPSIS Reports on Purview RBAC roles and their effective membership, including groups expanded recursively. .DESCRIPTION Produces a report of the membership of Purview RBAC role groups. By default, the report contains only the roles that have at least one member. When a role member is itself a group (distribution group, mail-enabled security group, dynamic distribution group, or a nested role group), its members are resolved recursively and included in the report with DirectMember set to $false and MemberViaGroup set to the name of the group that is a direct member of the role. Circular group references are detected and skipped automatically. .PARAMETER ShowGraph Reserved for future use. When specified, displays a graphical representation of the role membership. .OUTPUTS [System.Collections.Generic.List[Object]] containing PSCustomObject rows with the following properties: Role, MemberName, MemberDisplayName, MemberPrimarySMTPAddres, MemberIsDirSynced, MemberObjectID, MemberRecipientTypeDetails, RoleDescription, DirectMember, MemberViaGroup. .EXAMPLE Get-PurviewRoleReport Retrieves the Purview RBAC role report, including recursive group expansion. .EXAMPLE Get-PurviewRoleReport | Where-Object { $_.DirectMember -eq $false } | Format-Table Role, MemberName, MemberViaGroup Lists all users/objects resolved through group membership, showing which group is a direct member of the role. .EXAMPLE Get-PurviewRoleReport | Export-Csv -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_purviewRoles.csv" -Encoding UTF8 Exports the full report (including group-expanded members) to a CSV file. .NOTES Requires ExchangeOnlineManagement module and an active Connect-IPPSSession session. .LINK https://ps365.clidsys.com/docs/commands/Get-PurviewRoleReport #> function Get-PurviewRoleReport { [CmdletBinding()] param ( [switch]$ShowGraph ) try { Import-Module ExchangeOnlineManagement -ErrorAction stop } catch { Write-Warning 'First, install the official Microsoft Import-Module ExchangeOnlineManagement module : Install-Module ExchangeOnlineManagement' return } # if at least one result, we are connected if (-not (Get-ConnectionInformation | Where-Object { $_.ConnectionUri -like '*.compliance.protection.outlook.com' })) { Connect-IPPSSession } # Recursively expands a group member and adds its individual members to $ResultList. # $VisitedGroups prevents infinite loops when circular group references exist. function Expand-PurviewGroupMember { param ( [Parameter(Mandatory = $true)] $Member, [Parameter(Mandatory = $true)] [string]$ParentGroupName, [Parameter(Mandatory = $true)] [string]$RoleName, [Parameter(Mandatory = $true)] [string]$RoleDescription, [Parameter(Mandatory = $true)] [System.Collections.Generic.List[Object]]$ResultList, [Parameter(Mandatory = $true)] [System.Collections.Generic.HashSet[string]]$VisitedGroups ) # Use ExchangeObjectId when available, fall back to PrimarySmtpAddress as deduplication key $groupKey = if ($Member.ExchangeObjectId) { $Member.ExchangeObjectId.ToString() } else { $Member.PrimarySmtpAddress } if (-not $VisitedGroups.Add($groupKey)) { return } try { # Role groups are expanded via Get-RoleGroupMember; all other group types via Get-DistributionGroupMember if ($Member.RecipientTypeDetails -eq 'RoleGroup') { $subMembers = @(Get-RoleGroupMember -Identity $Member.ExchangeObjectId -ResultSize Unlimited -ErrorAction Stop) } else { $subMembers = @(Get-DistributionGroupMember -Identity $Member.PrimarySmtpAddress -ResultSize Unlimited -ErrorAction Stop) } } catch { Write-Warning "Could not expand group '$($Member.Name)': $($_.Exception.Message)" return } foreach ($subMember in $subMembers) { $object = [PSCustomObject][ordered]@{ 'Role' = $RoleName 'MemberName' = $subMember.Name 'MemberDisplayName' = $subMember.DisplayName 'MemberPrimarySMTPAddres' = $subMember.PrimarySmtpAddress 'MemberIsDirSynced' = $subMember.IsDirSynced 'MemberObjectID' = $subMember.ExternalDirectoryObjectId 'MemberRecipientTypeDetails' = $subMember.RecipientTypeDetails 'RoleDescription' = $RoleDescription 'DirectMember' = $false 'MemberViaGroup' = $ParentGroupName } $ResultList.Add($object) # Recurse into any nested group if ($subMember.RecipientTypeDetails -like '*Group*') { Expand-PurviewGroupMember -Member $subMember ` -ParentGroupName $ParentGroupName ` -RoleName $RoleName ` -RoleDescription $RoleDescription ` -ResultList $ResultList ` -VisitedGroups $VisitedGroups } } } try { # -ShowPartnerLinked : # This ShowPartnerLinked switch specifies whether to return built-in role groups that are of type PartnerRoleGroup. You don't need to specify a value with this switch. # This type of role group is used in the cloud-based service to allow partner service providers to manage their customer organizations. # These types of role groups can't be edited and are not shown by default. $purviewRoles = Get-RoleGroup -ShowPartnerLinked -ErrorAction Stop } catch { Write-Warning 'You are not connected to the Purview service. Please connect using Connect-IPPSSession.' return } [System.Collections.Generic.List[Object]]$purviewRolesMembership = @() foreach ($purviewRole in $purviewRoles) { try { # we need to use ExchangeObjectId instead of `Identity` or `DistinguishedName` otherwise we get the following error: # Get-RoleGroupMember: Ex9E65A2|Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException|The operation couldn't be performed because object: # 'FFO.extest.microsoft.com/Microsoft Exchange Hosted Organizations/xxx.onmicrosoft.com/Configuration/OrganizationManagement' matches multiple entries. $roleMembers = @(Get-RoleGroupMember -Identity $purviewRole.ExchangeObjectId -ResultSize Unlimited) # Add green color if member found into the role if ($roleMembers.count -gt 0) { Write-Host -ForegroundColor Green "Role $($purviewRole.Name) - Member(s) found: $($roleMembers.count)" } else { Write-Host -ForegroundColor Cyan "Role $($purviewRole.Name) - Member found: $($roleMembers.count)" } if ($purviewRole.Description -eq '' -and $purviewRole.Name -like 'ISVMailboxUsers_*') { $roleDescription = 'Third-party application developer mailbox role' } else { $roleDescription = $purviewRole.Description } if ($roleMembers.count -eq 0) { $object = [PSCustomObject][ordered]@{ 'Role' = $purviewRole.Name 'MemberName' = '-' 'MemberDisplayName' = '-' 'MemberPrimarySMTPAddres' = '-' 'MemberIsDirSynced' = '-' 'MemberObjectID' = '-' 'MemberRecipientTypeDetails' = '-' 'RoleDescription' = $roleDescription 'DirectMember' = '-' 'MemberViaGroup' = '-' } $purviewRolesMembership.Add($object) } else { # Fresh set per role to allow the same group to appear in multiple roles $visitedGroups = [System.Collections.Generic.HashSet[string]]::new() foreach ($roleMember in $roleMembers) { $object = [PSCustomObject][ordered]@{ 'Role' = $purviewRole.Name 'MemberName' = $roleMember.Name 'MemberDisplayName' = $roleMember.DisplayName 'MemberPrimarySMTPAddres' = $roleMember.PrimarySmtpAddress 'MemberIsDirSynced' = $roleMember.IsDirSynced 'MemberObjectID' = $roleMember.ExternalDirectoryObjectId 'MemberRecipientTypeDetails' = $roleMember.RecipientTypeDetails 'RoleDescription' = $roleDescription 'DirectMember' = $true 'MemberViaGroup' = '-' } $purviewRolesMembership.Add($object) # Expand group members recursively if ($roleMember.RecipientTypeDetails -like '*Group*') { Expand-PurviewGroupMember -Member $roleMember ` -ParentGroupName $roleMember.Name ` -RoleName $purviewRole.Name ` -RoleDescription $roleDescription ` -ResultList $purviewRolesMembership ` -VisitedGroups $visitedGroups } } } } catch { Write-Warning $_.Exception.Message } } return $purviewRolesMembership } |