Public/Get-EmptyGPOs.ps1

function Get-EmptyGPOs {
    <#
    .SYNOPSIS
        Finds Group Policy Objects with no settings configured.
 
    .DESCRIPTION
        Retrieves every GPO in the domain and examines the XML report to determine
        whether Computer Configuration and User Configuration contain any extension
        data (actual policy settings). GPOs where both sides are empty clutter GPMC
        and add to processing overhead without providing any value.
 
        When -IncludeDisabledSections is specified, the function also flags GPOs where
        one configuration side is disabled but still contains settings -- a common
        misconfiguration that hides active policies.
 
        This function is read-only and never modifies or deletes GPOs.
 
    .PARAMETER IncludeDisabledSections
        Also report GPOs where a configuration section (Computer or User) is disabled
        but still has settings configured. These settings are dormant and may indicate
        a misconfiguration.
 
    .EXAMPLE
        Get-EmptyGPOs
 
        Returns GPOs where both Computer and User configurations have zero settings.
 
    .EXAMPLE
        Get-EmptyGPOs -IncludeDisabledSections
 
        Also includes GPOs with disabled-but-configured sections.
 
    .OUTPUTS
        [PSCustomObject] with properties: DisplayName, Id, ComputerSettingsConfigured,
        UserSettingsConfigured, ComputerEnabled, UserEnabled, Finding
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$IncludeDisabledSections
    )

    begin {
        Write-Verbose 'Get-EmptyGPOs: Starting scan for empty Group Policy Objects'

        if (-not (Get-Module -ListAvailable -Name GroupPolicy)) {
            throw 'The GroupPolicy RSAT module is required but not installed. Install RSAT tools and try again.'
        }
        Import-Module GroupPolicy -ErrorAction Stop -Verbose:$false
    }

    process {
        try {
            $AllGPOs = @(Get-GPO -All -ErrorAction Stop)
            Write-Verbose "Get-EmptyGPOs: Retrieved $($AllGPOs.Count) GPOs from domain"
        }
        catch {
            Write-Error "Failed to retrieve GPOs: $_"
            return
        }

        $EmptyCount = 0

        foreach ($GPO in $AllGPOs) {
            Write-Verbose "Get-EmptyGPOs: Checking settings for '$($GPO.DisplayName)'"

            try {
                [xml]$Report = Get-GPOReport -Guid $GPO.Id -ReportType Xml -ErrorAction Stop
            }
            catch {
                Write-Warning "Get-EmptyGPOs: Could not generate report for '$($GPO.DisplayName)': $_"
                continue
            }

            # Determine whether each configuration side has extension data
            $ComputerExtensions = $Report.GPO.Computer.ExtensionData
            $UserExtensions     = $Report.GPO.User.ExtensionData

            $ComputerHasSettings = ($null -ne $ComputerExtensions) -and (@($ComputerExtensions).Count -gt 0)
            $UserHasSettings     = ($null -ne $UserExtensions) -and (@($UserExtensions).Count -gt 0)

            # Determine enabled state from the GPO object
            $ComputerEnabled = $GPO.GpoStatus -in @('AllSettingsEnabled', 'UserSettingsDisabled')
            $UserEnabled     = $GPO.GpoStatus -in @('AllSettingsEnabled', 'ComputerSettingsDisabled')

            # Case 1: Completely empty GPO (no settings on either side)
            if (-not $ComputerHasSettings -and -not $UserHasSettings) {
                $EmptyCount++
                [PSCustomObject]@{
                    DisplayName                = $GPO.DisplayName
                    Id                         = $GPO.Id.ToString()
                    ComputerSettingsConfigured = $false
                    UserSettingsConfigured      = $false
                    ComputerEnabled            = $ComputerEnabled
                    UserEnabled                = $UserEnabled
                    Finding                    = 'EMPTY'
                }
            }
            # Case 2: Disabled section that still has settings (only with switch)
            elseif ($IncludeDisabledSections) {
                $Findings = @()

                if ($ComputerHasSettings -and -not $ComputerEnabled) {
                    $Findings += 'COMPUTER_DISABLED_WITH_SETTINGS'
                }
                if ($UserHasSettings -and -not $UserEnabled) {
                    $Findings += 'USER_DISABLED_WITH_SETTINGS'
                }

                if ($Findings.Count -gt 0) {
                    $EmptyCount++
                    [PSCustomObject]@{
                        DisplayName                = $GPO.DisplayName
                        Id                         = $GPO.Id.ToString()
                        ComputerSettingsConfigured = $ComputerHasSettings
                        UserSettingsConfigured      = $UserHasSettings
                        ComputerEnabled            = $ComputerEnabled
                        UserEnabled                = $UserEnabled
                        Finding                    = $Findings -join '; '
                    }
                }
            }
        }

        Write-Verbose "Get-EmptyGPOs: Scan complete. Found $EmptyCount finding(s) out of $($AllGPOs.Count) total GPOs."
    }
}