Public/Entra/Group/Get-EmptyGroup.ps1

<#
    .SYNOPSIS
    Retrieves all empty groups (zero members) in Microsoft Entra ID.
 
    .DESCRIPTION
    The Get-EmptyGroup function retrieves all groups from Microsoft Entra ID via the
    Microsoft Graph API and identifies those with no members. It supports optional
    export to Excel or CSV for further analysis and cleanup.
 
    .PARAMETER ExportToExcel
    When specified, exports the results to an Excel file in the user's profile directory.
    Requires the ImportExcel module.
 
    .PARAMETER ExportCsv
    Optional path to export the results as a CSV file.
 
    .EXAMPLE
    Get-EmptyGroup
 
    Retrieves all empty groups and outputs them to the console.
 
    .EXAMPLE
    Get-EmptyGroup -ExportToExcel
 
    Retrieves all empty groups and exports the results to an Excel file.
 
    .EXAMPLE
    Get-EmptyGroup -ExportCsv 'C:\temp\empty-groups.csv'
 
    Retrieves all empty groups and exports the results to a CSV file.
 
    .OUTPUTS
    System.Collections.Generic.List[Object]
 
    .NOTES
    OUTPUT PROPERTIES
    Returns a collection of custom objects with the following properties:
    - DisplayName: Display name of the group
    - ObjectId: Unique identifier of the group
    - Type: Type of group (Microsoft 365, Dynamic, Mail-enabled Security, Security, Distribution, Other)
    - Mail: Primary email address of the group
    - Created: Creation date of the group (yyyy-MM-dd)
    - Description: Description of the group
 
    Requires Microsoft.Graph module: Connect-MgGraph -Scopes 'Group.Read.All'
 
    .LINK
    https://ps365.clidsys.com/docs/commands/Get-EmptyGroup
#>


function Get-EmptyGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [switch]$ExportToExcel
    )

    Write-Verbose 'Fetching all groups...'

    # Fetch all groups with pagination
    [System.Collections.Generic.List[Object]]$allGroups = @()
    $uri = "https://graph.microsoft.com/v1.0/groups?`$select=id,displayName,groupTypes,securityEnabled,mailEnabled,mail,createdDateTime,description&`$top=999&`$count=true"
    $headers = @{ ConsistencyLevel = 'eventual' }

    do {
        $response = Invoke-MgGraphRequest -Method GET -Uri $uri -Headers $headers
        foreach ($group in $response.value) {
            $allGroups.Add($group)
        }
        $uri = $response.'@odata.nextLink'
    } while ($uri)

    $totalCount = $allGroups.Count
    Write-Verbose "Found $totalCount groups. Scanning for empty groups..."
    Write-Host -ForegroundColor Cyan "Found $totalCount groups. Scanning for empty groups..."

    # Check member count for each group
    [System.Collections.Generic.List[Object]]$emptyGroups = @()
    $processed = 0

    foreach ($group in $allGroups) {
        $processed++
        if ($processed % 50 -eq 0) {
            Write-Progress -Activity 'Scanning group members' -Status "$processed / $totalCount" -PercentComplete (($processed / $totalCount) * 100)
        }

        try {
            $membersUri = "https://graph.microsoft.com/v1.0/groups/$($group.id)/members/`$count"
            $memberCount = Invoke-MgGraphRequest -Method GET -Uri $membersUri -Headers @{ ConsistencyLevel = 'eventual' }

            if ($memberCount -eq 0) {
                # Determine group type
                $type = if ($group.groupTypes -contains 'Unified') { 'Microsoft 365' }
                        elseif ($group.groupTypes -contains 'DynamicMembership') { 'Dynamic' }
                        elseif ($group.securityEnabled -and $group.mailEnabled) { 'Mail-enabled Security' }
                        elseif ($group.securityEnabled) { 'Security' }
                        elseif ($group.mailEnabled) { 'Distribution' }
                        else { 'Other' }

                $object = [PSCustomObject][ordered]@{
                    DisplayName = $group.displayName
                    ObjectId    = $group.id
                    Type        = $type
                    Mail        = $group.mail
                    Created     = if ($group.createdDateTime) { ([datetime]$group.createdDateTime).ToString('yyyy-MM-dd') } else { '' }
                    Description = $group.description
                }

                $emptyGroups.Add($object)
            }
        }
        catch {
            Write-Warning "Failed to check members for group '$($group.displayName)': $_"
        }
    }

    Write-Progress -Activity 'Scanning group members' -Completed

    Write-Host -ForegroundColor Yellow "`nEmpty Groups: $($emptyGroups.Count) / $totalCount total`n"

    if ($emptyGroups.Count -eq 0) {
        Write-Host -ForegroundColor Green 'No empty groups found.'
        return
    }

    if ($ExportToExcel.IsPresent) {
        Write-Verbose 'Preparing Excel export...'
        $now = Get-Date -Format 'yyyy-MM-dd_HHmmss'
        $excelFilePath = "$($env:USERPROFILE)\$now-EmptyGroups.xlsx"
        Write-Verbose "Excel file path: $excelFilePath"
        Write-Host -ForegroundColor Cyan "Exporting empty groups to Excel file: $excelFilePath"
        $emptyGroups | Sort-Object DisplayName | Export-Excel -Path $excelFilePath -AutoSize -AutoFilter -WorksheetName 'EmptyGroups'
        Write-Host -ForegroundColor Green 'Export completed successfully!'
    }
    else {
        Write-Verbose "Returning $($emptyGroups.Count) empty groups"
        return $emptyGroups
    }
}