Public/Get-IOOwnerlessGroups.ps1

function Get-IOOwnerlessGroups {
    <#
    .SYNOPSIS
        Finds Microsoft 365 and Security groups with no owners.
    .EXAMPLE
        Get-IOOwnerlessGroups
    .EXAMPLE
        Get-IOOwnerlessGroups -GroupType M365 -ToCsv "ownerless-groups.csv"
    #>

    [CmdletBinding()]
    param(
        [ValidateSet('All', 'M365', 'Security', 'MailEnabled')]
        [string]$GroupType = 'All',

        [string]$ToCsv
    )

    $cmdName = $MyInvocation.MyCommand.Name
    Write-IOLog "Scanning for groups without owners (Type: $GroupType)..." -Level Info -Component $cmdName

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Build filter
    $filter = switch ($GroupType) {
        'M365'        { "`$filter=groupTypes/any(g:g eq 'Unified')&" }
        'Security'    { "`$filter=securityEnabled eq true and mailEnabled eq false&" }
        'MailEnabled' { "`$filter=mailEnabled eq true&" }
        default       { '' }
    }

    $groups = Invoke-IOGraphRequest -Uri "v1.0/groups?${filter}`$select=id,displayName,groupTypes,securityEnabled,mailEnabled,mail,createdDateTime,membershipRule"

    $total   = ($groups | Measure-Object).Count
    $counter = 0

    if ($total -eq 0) {
        Export-IOResult -Data @() -ToCsv $ToCsv -CommandName $cmdName
        return
    }

    try {
    foreach ($grp in $groups) {
        $counter++
        if ($counter % 50 -eq 0) {
            Write-Progress -Activity 'Checking group owners' -Status "$counter / $total" -PercentComplete (($counter / $total) * 100)
        }

        try {
            $owners = Invoke-IOGraphRequest -Uri "v1.0/groups/$($grp.id)/owners?`$select=id" -NoPagination -SkipConnectionCheck
        }
        catch {
            $owners = @()
        }

        if (-not $owners -or $owners.Count -eq 0) {
            $type = if ($grp.groupTypes -contains 'Unified') { 'Microsoft365' }
                    elseif ($grp.securityEnabled) { 'Security' }
                    elseif ($grp.mailEnabled) { 'MailEnabled' }
                    else { 'Other' }

            $isDynamic = if ($grp.membershipRule) { $true } else { $false }
            $created = if ($grp.createdDateTime) { [datetime]::Parse($grp.createdDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal).ToString('yyyy-MM-dd') } else { 'Unknown' }

            $results.Add([PSCustomObject]@{
                GroupName     = $grp.displayName
                GroupId       = $grp.id
                GroupType     = $type
                Mail          = $grp.mail
                IsDynamic     = $isDynamic
                CreatedDate   = $created
                OwnerCount    = 0
            })
        }
    }
    }
    finally {
        Write-Progress -Activity 'Checking group owners' -Completed
    }

    $sorted = $results | Sort-Object GroupType, GroupName
    Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName
}