Private/Get-EntraGroupMembers.ps1
|
function Resolve-EntraGroup { <# Resolve an Entra (security / M365) group to its PEOPLE via Graph, using the transitive membership so nested groups are flattened. The site-centric counterpart to Test-UserIsGroupMember: there we ask "is this ONE user in the group"; here we ask "how many people are in it" and, on request, who. The microsoft.graph.user cast keeps only USER members - nested groups, devices and service principals drop out - so the number genuinely means "people". Returns { Count; Members } where Members is an array of { Upn; Display } (empty unless -IncludeMembers). Count is $null when Graph cannot answer (no scope / failure), so a caller shows a blank, never a wrong 0. Cached per (group, include-members) in the supplied hashtable - one group can appear on many objects across a scan. Needs Graph GroupMember.Read.All. #> param( [Parameter(Mandatory)] [string] $GroupId, [hashtable] $Cache = @{}, [switch] $IncludeMembers ) $key = "grp|$GroupId|$([bool]$IncludeMembers)" if ($Cache.ContainsKey($key)) { return $Cache[$key] } $result = [pscustomobject]@{ Count = $null; Members = @() } try { # $count + ConsistencyLevel:eventual is what makes @odata.count come back. $url = "groups/$GroupId/transitiveMembers/microsoft.graph.user" + '?$count=true&$select=userPrincipalName,displayName&$top=999' $people = [System.Collections.Generic.List[object]]::new() $count = $null do { $resp = Invoke-PnPGraphMethod -Url $url -Method Get -AdditionalHeaders @{ ConsistencyLevel = 'eventual' } if ($null -eq $count -and ($resp.PSObject.Properties.Name -contains '@odata.count')) { $count = [int]$resp.'@odata.count' } if ($IncludeMembers -and ($resp.PSObject.Properties.Name -contains 'value')) { foreach ($u in @($resp.value)) { $people.Add([pscustomobject]@{ Upn = "$($u.userPrincipalName)"; Display = "$($u.displayName)" }) } } # Only page when we actually want the members; a count needs one call. $url = if ($IncludeMembers -and ($resp.PSObject.Properties.Name -contains '@odata.nextLink')) { "$($resp.'@odata.nextLink')" } else { $null } } while ($url) # When we pulled the people, trust that count (nextLink pages omit @odata.count). if ($IncludeMembers) { $count = $people.Count } $result = [pscustomobject]@{ Count = $count; Members = $people.ToArray() } } catch { Write-Verbose "Could not resolve Entra group $GroupId : $($_.Exception.Message)" } $Cache[$key] = $result return $result } |