Private/Resolve-SPMEntraGroup.ps1
|
function Resolve-SPMEntraGroup { <# .SYNOPSIS Resolves the members of an Entra ID group transitively via Microsoft Graph. .DESCRIPTION Uses Invoke-PnPGraphMethod (same token as the PnP connection, no extra Graph module). Nested groups are resolved recursively while keeping the nesting visible in the resulting tree. Every group is resolved only once per scan ($GroupCache), cycles are detected via $Visited. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$GroupId, [Parameter(Mandatory)] [hashtable]$GroupCache, [ValidateSet('members', 'owners')] [string]$Relation = 'members', [System.Collections.Generic.HashSet[string]]$Visited ) $cacheKey = "$GroupId|$Relation" if ($GroupCache.ContainsKey($cacheKey)) { return $GroupCache[$cacheKey] } if (-not $Visited) { $Visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) } if (-not $Visited.Add($GroupId)) { return , @([pscustomobject]@{ name = '(cycle - group already resolved further up)'; type = 'Note' }) } $members = [System.Collections.Generic.List[object]]::new() try { $url = "v1.0/groups/$GroupId/$Relation`?`$select=id,displayName,userPrincipalName&`$top=999" while ($url) { $response = Invoke-SPMWithRetry { Invoke-PnPGraphMethod -Url $url } foreach ($m in @($response.value)) { switch ($m.'@odata.type') { '#microsoft.graph.group' { $members.Add([pscustomobject]@{ name = $m.displayName type = 'EntraGroup' members = @(Resolve-SPMEntraGroup -GroupId $m.id -GroupCache $GroupCache -Visited $Visited) }) } '#microsoft.graph.user' { $members.Add([pscustomobject]@{ name = $m.displayName type = 'User' upn = $m.userPrincipalName }) } default { $label = if ($m.displayName) { $m.displayName } else { [string]$m.id } $members.Add([pscustomobject]@{ name = $label; type = 'Other' }) } } } $url = $response.'@odata.nextLink' } } catch { Write-Warning "Could not resolve $Relation of Entra group ${GroupId}: $($_.Exception.Message)" $members.Add([pscustomobject]@{ name = '(resolution failed - is the Graph permission Directory.Read.All granted?)'; type = 'Note' }) } $result = $members.ToArray() $GroupCache[$cacheKey] = $result [void]$Visited.Remove($GroupId) return $result } |