Public/Get-GkGroupReport.ps1
|
function Get-GkGroupReport { <# .SYNOPSIS Report groups with their type (Microsoft 365 / security / distribution / dynamic), membership count, owners, and an ownerless flag. .DESCRIPTION Reads GET /groups and classifies each group from groupTypes/securityEnabled/mailEnabled. For each group it also fetches the membership count (GET /groups/{id}/members/$count, which requires the ConsistencyLevel: eventual header) and the owners (GET /groups/{id}/owners), flagging ownerless groups. Caveat on ownerless detection: owners are not returned by Graph for groups created in Exchange, distribution groups, or on-premises-synced groups, so IsOwnerless can be a false positive for those. The GroupType column lets you account for that. Membership counting is one call per group; use -SkipMemberCount to omit it on large tenants. .PARAMETER GroupType Client-side filter: All (default), Microsoft365, Security, MailEnabledSecurity, or Distribution. .PARAMETER OwnerlessOnly Return only groups with no owners (see the ownerless caveat above). .PARAMETER SkipMemberCount Do not fetch per-group membership counts (MemberCount will be $null). .PARAMETER AsReport Flatten Owners to a '; '-joined string and add ReportGeneratedUtc. .EXAMPLE Get-GkGroupReport -OwnerlessOnly | Where-Object GroupType -eq 'Microsoft365' Ownerless Microsoft 365 groups — a governance cleanup list. .EXAMPLE Get-GkGroupReport | Sort-Object MemberCount -Descending | Select-Object -First 20 The 20 largest groups by membership. .EXAMPLE Get-GkGroupReport -SkipMemberCount -AsReport | Export-Csv .\groups.csv -NoTypeInformation .OUTPUTS PSGraphKit.GroupReport #> [CmdletBinding()] [OutputType('PSGraphKit.GroupReport')] param( [ValidateSet('All', 'Microsoft365', 'Security', 'MailEnabledSecurity', 'Distribution')] [string] $GroupType = 'All', [switch] $OwnerlessOnly, [switch] $SkipMemberCount, [switch] $AsReport ) begin { Test-GkConnection -FunctionName 'Get-GkGroupReport' | Out-Null $now = [datetime]::UtcNow } process { $select = 'id,displayName,mail,groupTypes,securityEnabled,mailEnabled,membershipRule,membershipRuleProcessingState,visibility' $groups = Invoke-GkGraphRequest -Uri "/groups?`$select=$select&`$top=999" -CallerFunction 'Get-GkGroupReport' $memberCountFailures = 0 $ownerFailures = 0 foreach ($g in $groups) { $id = [string](Get-GkDictValue $g 'id') $groupTypes = @(Get-GkDictValue $g 'groupTypes') $secEnabled = [bool](Get-GkDictValue $g 'securityEnabled') $mailEnab = [bool](Get-GkDictValue $g 'mailEnabled') $isDynamic = ($groupTypes -contains 'DynamicMembership') $type = if ($groupTypes -contains 'Unified') { 'Microsoft365' } elseif ($secEnabled -and $mailEnab) { 'MailEnabledSecurity' } elseif ($secEnabled -and -not $mailEnab) { 'Security' } elseif ($mailEnab -and -not $secEnabled) { 'Distribution' } else { 'Unknown' } if ($GroupType -ne 'All' -and $type -ne $GroupType) { continue } # Members count (one call per group unless skipped). $memberCount = $null if (-not $SkipMemberCount -and $id) { try { # NOTE: /members/$count returns text/plain, which breaks -OutputType Hashtable. # Read @odata.count from a $count=true collection query (JSON) instead; $top=1 # keeps the payload tiny. Requires the ConsistencyLevel: eventual header. $countResp = Invoke-GkGraphRequest -Raw -CallerFunction 'Get-GkGroupReport' ` -Uri "/groups/$id/members?`$count=true&`$top=1" -Headers @{ ConsistencyLevel = 'eventual' } $memberCount = [int](Get-GkDictValue $countResp '@odata.count') } catch { $memberCountFailures++ Write-Verbose "PSGraphKit: member count unavailable for group $id : $($_.Exception.Message)" } } # Owners. $ownerNames = @() if ($id) { try { $owners = Invoke-GkGraphRequest -CallerFunction 'Get-GkGroupReport' -Uri "/groups/$id/owners?`$select=id,displayName" $ownerNames = @($owners | ForEach-Object { [string](Get-GkDictValue $_ 'displayName') } | Where-Object { $_ }) } catch { $ownerFailures++ Write-Verbose "PSGraphKit: owners unavailable for group $id : $($_.Exception.Message)" } } if ($OwnerlessOnly -and $ownerNames.Count -gt 0) { continue } $obj = [ordered]@{ PSTypeName = 'PSGraphKit.GroupReport' DisplayName = [string](Get-GkDictValue $g 'displayName') GroupType = $type IsDynamic = $isDynamic MemberCount = $memberCount Owners = if ($AsReport) { $ownerNames -join '; ' } else { $ownerNames } OwnerCount = $ownerNames.Count IsOwnerless = ($ownerNames.Count -eq 0) Mail = [string](Get-GkDictValue $g 'mail') Visibility = [string](Get-GkDictValue $g 'visibility') MembershipRule = [string](Get-GkDictValue $g 'membershipRule') Id = $id } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } if ($memberCountFailures -gt 0) { Write-Warning "Membership count could not be read for $memberCountFailures group(s) — MemberCount is null for those. Needs GroupMember.Read.All (or Group.Read.All) and the ConsistencyLevel: eventual header." } if ($ownerFailures -gt 0) { Write-Warning "Owner lookup failed for $ownerFailures group(s) — IsOwnerless may be inaccurate for those (as opposed to genuinely having no owners)." } } } |