Public/Get-UnlinkedGPOs.ps1
|
function Get-UnlinkedGPOs { <# .SYNOPSIS Finds Group Policy Objects that are not linked to any OU, site, or domain. .DESCRIPTION Retrieves every GPO in the domain, generates an XML report for each, and checks for the presence of <LinksTo> elements. GPOs with zero links are returned as findings. Unlinked GPOs still consume SYSVOL space and appear in GPMC but never apply to any object -- they are safe deletion candidates. This function is read-only and never modifies or deletes GPOs. .EXAMPLE Get-UnlinkedGPOs Returns all GPOs in the current domain that have no links. .EXAMPLE Get-UnlinkedGPOs | Export-Csv -Path .\unlinked.csv -NoTypeInformation Exports unlinked GPO findings to CSV for offline review. .OUTPUTS [PSCustomObject] with properties: DisplayName, Id, CreationTime, ModificationTime, Owner, Finding #> [CmdletBinding()] param() begin { Write-Verbose 'Get-UnlinkedGPOs: Starting scan for unlinked Group Policy Objects' # Validate that the GroupPolicy module is available 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-UnlinkedGPOs: Retrieved $($AllGPOs.Count) GPOs from domain" } catch { Write-Error "Failed to retrieve GPOs: $_" return } $UnlinkedCount = 0 foreach ($GPO in $AllGPOs) { Write-Verbose "Get-UnlinkedGPOs: Checking links for '$($GPO.DisplayName)'" try { [xml]$Report = Get-GPOReport -Guid $GPO.Id -ReportType Xml -ErrorAction Stop } catch { Write-Warning "Get-UnlinkedGPOs: Could not generate report for '$($GPO.DisplayName)': $_" continue } # The GPO XML report uses a namespace -- check for LinksTo elements $NamespaceManager = New-Object System.Xml.XmlNamespaceManager($Report.NameTable) $NamespaceManager.AddNamespace('gpo', 'http://www.microsoft.com/GroupPolicy/Settings') $Links = $Report.SelectNodes('//gpo:LinksTo', $NamespaceManager) # Fallback: also try without namespace (some report formats differ) if ($null -eq $Links -or $Links.Count -eq 0) { $Links = $Report.GPO.LinksTo } if ($null -eq $Links -or @($Links).Count -eq 0) { $UnlinkedCount++ [PSCustomObject]@{ DisplayName = $GPO.DisplayName Id = $GPO.Id.ToString() CreationTime = $GPO.CreationTime ModificationTime = $GPO.ModificationTime Owner = $GPO.Owner Finding = 'UNLINKED' } } } Write-Verbose "Get-UnlinkedGPOs: Scan complete. Found $UnlinkedCount unlinked GPO(s) out of $($AllGPOs.Count) total." } } |