Src/Private/Get-AbrEntraIDGroups.ps1

function Get-AbrEntraIDGroups {
    <#
    .SYNOPSIS
    Documents Entra ID group inventory including dynamic groups and membership counts.
    .DESCRIPTION
        Collects and reports on:
          - Group summary statistics (totals, types, dynamic vs assigned)
          - Group inventory (name, type, membership rule, mail-enabled, owner count)
          - Dynamic group detail (membership rules) at InfoLevel 2
    .NOTES
        Version: 0.1.21
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Entra ID Groups for tenant $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Groups'
    }

    process {
        #region Groups
        # Section{} created unconditionally so catch{} always writes inside it
        Section -Style Heading2 'Groups' {
            Paragraph "The following section documents the groups configured in tenant $TenantId."
            BlankLine

            try {
                Write-Host " - Retrieving groups..."
                $Groups = Get-MgGroup -All `
                    -Property Id,DisplayName,GroupTypes,MailEnabled,SecurityEnabled,MembershipRule,MembershipRuleProcessingState,CreatedDateTime,Description,Mail,OnPremisesSyncEnabled `
                    -ErrorAction Stop

                if ($Groups) {

                    #region Group Summary
                    $SecurityGroups   = @($Groups | Where-Object { $_.SecurityEnabled -and $_.GroupTypes -notcontains 'Unified' }).Count
                    $M365Groups       = @($Groups | Where-Object { $_.GroupTypes -contains 'Unified' }).Count
                    $DynamicGroups    = @($Groups | Where-Object { $_.GroupTypes -contains 'DynamicMembership' }).Count
                    $AssignedGroups   = @($Groups | Where-Object { $_.GroupTypes -notcontains 'DynamicMembership' }).Count
                    $MailEnabled      = @($Groups | Where-Object { $_.MailEnabled }).Count
                    $SyncedGroups     = @($Groups | Where-Object { $_.OnPremisesSyncEnabled -eq $true }).Count

                    $GrpSumObj = [System.Collections.ArrayList]::new()
                    $grpSumInObj = [ordered] @{
                        'Total Groups'               = @($Groups).Count
                        'Security Groups'            = $SecurityGroups
                        'Microsoft 365 Groups'       = $M365Groups
                        'Dynamic Membership Groups'  = $DynamicGroups
                        'Assigned Membership Groups' = $AssignedGroups
                        'Mail-Enabled Groups'        = $MailEnabled
                        'Hybrid (On-Prem Synced)'    = $SyncedGroups
                    }
                    $GrpSumObj.Add([pscustomobject]$grpSumInObj) | Out-Null

                    $GrpSumTableParams = @{ Name = "Group Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $GrpSumTableParams['Caption'] = "- $($GrpSumTableParams.Name)" }
                    $GrpSumObj | Table @GrpSumTableParams
                    #endregion

                    #region Group Inventory Table
                    $GrpObj = [System.Collections.ArrayList]::new()
                    foreach ($Group in ($Groups | Sort-Object DisplayName)) {
                        try {
                            $GroupType = if ($Group.GroupTypes -contains 'Unified') { 'Microsoft 365' }
                                         elseif ($Group.SecurityEnabled -and $Group.MailEnabled) { 'Mail-Enabled Security' }
                                         elseif ($Group.SecurityEnabled) { 'Security' }
                                         elseif ($Group.MailEnabled) { 'Distribution' }
                                         else { 'Other' }

                            $MembershipType = if ($Group.GroupTypes -contains 'DynamicMembership') { 'Dynamic' } else { 'Assigned' }

                            $grpInObj = [ordered] @{
                                'Group Name'         = $Group.DisplayName
                                'Type'               = $GroupType
                                'Membership'         = $MembershipType
                                'Mail-Enabled'       = if ($Group.MailEnabled) { 'Yes' } else { 'No' }
                                'Security-Enabled'   = if ($Group.SecurityEnabled) { 'Yes' } else { 'No' }
                                'Hybrid Synced'      = if ($Group.OnPremisesSyncEnabled) { 'Yes' } else { 'No' }
                                'Created'            = if ($Group.CreatedDateTime) { ($Group.CreatedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                            }
                            $GrpObj.Add([pscustomobject](ConvertTo-HashToYN $grpInObj)) | Out-Null
                        } catch {
                            Write-PScriboMessage -IsWarning -Message "Group '$($Group.DisplayName)': $($_.Exception.Message)"
                        }
                    }

                    $GrpTableParams = @{ Name = "Group Inventory - $TenantId"; List = $false; ColumnWidths = 28, 16, 12, 10, 12, 12, 10 }
                    if ($Report.ShowTableCaptions) { $GrpTableParams['Caption'] = "- $($GrpTableParams.Name)" }
                    $GrpObj | Table @GrpTableParams

                    $null = ($script:ExcelSheets['Groups'] = $GrpObj)
                    #endregion

                    #region Dynamic Group Detail (InfoLevel 2)
                    if ($InfoLevel.Groups -ge 2) {
                        $DynamicGroupList = $Groups | Where-Object { $_.GroupTypes -contains 'DynamicMembership' }

                        if ($DynamicGroupList) {
                            Section -Style Heading3 'Dynamic Group Membership Rules' {
                                Paragraph "The following $(@($DynamicGroupList).Count) dynamic group(s) are configured in tenant $TenantId. Review membership rules regularly to ensure they target the intended user/device population."
                                BlankLine

                                $DynObj = [System.Collections.ArrayList]::new()
                                foreach ($DynGroup in ($DynamicGroupList | Sort-Object DisplayName)) {
                                    $ProcessingState = switch ($DynGroup.MembershipRuleProcessingState) {
                                        'On'     { 'Active' }
                                        'Paused' { 'Paused' }
                                        default  { if ($DynGroup.MembershipRuleProcessingState) { $DynGroup.MembershipRuleProcessingState } else { '--' } }
                                    }

                                    $dynInObj = [ordered] @{
                                        'Group Name'        = $DynGroup.DisplayName
                                        'Processing State'  = $ProcessingState
                                        'Membership Rule'   = if ($DynGroup.MembershipRule) { $DynGroup.MembershipRule } else { 'No rule defined' }
                                    }
                                    $DynObj.Add([pscustomobject]$dynInObj) | Out-Null
                                }

                                $null = (& {
                                    if ($HealthCheck.EntraID.Groups) {
                                        $null = ($DynObj | Where-Object { $_.'Processing State' -eq 'Paused' } | Set-Style -Style Warning | Out-Null)
                                        $null = ($DynObj | Where-Object { $_.'Membership Rule' -eq 'No rule defined' } | Set-Style -Style Critical | Out-Null)
                                    }
                                })

                                $DynTableParams = @{ Name = "Dynamic Group Rules - $TenantId"; List = $false; ColumnWidths = 28, 16, 56 }
                                if ($Report.ShowTableCaptions) { $DynTableParams['Caption'] = "- $($DynTableParams.Name)" }
                                $DynObj | Table @DynTableParams
                                $null = ($script:ExcelSheets['Dynamic Groups'] = $DynObj)
                            }
                        }
                    }
                    #endregion

                } else {
                    Paragraph "No groups were found in tenant $TenantId."
                }

            } catch {
                Write-AbrSectionError -Section 'Groups section' -Message "$($_.Exception.Message)"
            }
        } # end Section Groups
        #endregion
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Groups'
    }
}