I often find myself opening the Group Policy Management Console, and wondering “are all of these GPOs actually being used? How would we know? Where do you even start?”.
So, I wanted to write a script that could trawl through all of the GPOs in my environment and tell me which ones are are candidates for a cleanup.
What Makes a GPO a Cleanup Candidate?
Before we start, let’s define what “unused” means for this script:
Unlinked — The GPO has no enabled links to any domain, OU, or AD site (most people don’t know or forget that GPOs can be linked to AD Sites).
Not applying anywhere — The GPO has no users/computers with “Apply Group Policy” permission (literally zero “Apply” entries).
All settings disabled — The GPO’s status is AllSettingsDisabled, meaning it won’t do anything even if linked.
This is a practical way to identify GPOs that are truly not used in your environment.
How the Script Works (Step-by-Step)
Here’s the logic the script implements:
Choose the PDC Emulator Active Directory replication can be inconsistent, especially for the Configuration partition where site links live; using the PDC Emulator means everyone sees the same data.
Collect enabled links The script reads the gPLink attribute from:
- The domain root
- All OUs
- All AD sites
Only links that are enabled are counted. Disabled links are ignored. This gives you a list of GPOs that are actively linked.
Check Security Filtering For each GPO, the script counts how many principals have “Apply Group Policy” permission. If the count is zero, that GPO may not apply to anything.
Check GPO status If a GPO has its status set to “AllSettingsDisabled”, it’s doing nothing even if it’s linked.
Combine results Any GPO that meets any of these conditions (no enabled link, no Apply principals, or AllSettingsDisabled) is included in the output.
Before deleting anything, make sure you back up GPOs and review them with your team.
Here’s the script:
Import-Module GroupPolicy -ErrorAction Stop
Import-Module ActiveDirectory -ErrorAction Stop
# Use the PDC Emulator as the single DC to query
$pdc = (Get-ADDomain).PDCEmulator
Write-Host "Using PDC Emulator: $pdc"
Write-Host ""
# Parse gPLink without to extract the GPO GUID and the link options number
function Get-GpLinkEntries {
param(
[object]$GpLink,
[string]$TargetDn,
[string]$TargetType
)
if ($null -eq $GpLink) { return @() }
# Flatten multi-valued attributes
if (($GpLink -is [System.Collections.IEnumerable]) -and -not ($GpLink -is [string])) {
$all = @()
foreach ($v in $GpLink) {
$all += Get-GpLinkEntries -GpLink $v -TargetDn $TargetDn -TargetType $TargetType
}
return $all
}
$text = [string]$GpLink
if ([string]::IsNullOrWhiteSpace($text)) { return @() }
$out = @()
# gPLink entries are bracketed: [LDAP://...;0][LDAP://...;2]
# Split by [ and ] to isolate each entry
$parts = $text -split '\['
foreach ($p in $parts) {
$entry = $p.Trim()
if ($entry.Length -eq 0) { continue }
# Remove trailing ] if present
if ($entry.EndsWith(']')) { $entry = $entry.Substring(0, $entry.Length - 1) }
# We only care about LDAP/LDAPS entries
$lower = $entry.ToLowerInvariant()
if (-not ($lower.StartsWith('ldap://') -or $lower.StartsWith('ldaps://'))) { continue }
# Split on semicolons; options number should be the last part
$semiParts = $entry.Split(';')
if ($semiParts.Count -lt 2) { continue }
$optText = $semiParts[$semiParts.Count - 1].Trim()
$opt = 0
if (-not [int]::TryParse($optText, [ref]$opt)) { continue }
# Extract GUID between { and }
$l = $entry.IndexOf('{')
$r = $entry.IndexOf('}', $l + 1)
if ($l -lt 0 -or $r -lt 0 -or $r -le $l) { continue }
$guidText = $entry.Substring($l + 1, $r - $l - 1).Trim()
$guid = $null
try { $guid = [Guid]$guidText } catch { continue }
$isDisabled = (($opt -band 1) -ne 0)
$out += [pscustomobject]@{
GpoId = $guid
Disabled = $isDisabled
TargetType = $TargetType
TargetDn = $TargetDn
}
}
return $out
}
# Track all GPOs that have at least one enabled link anywhere
$enabledSet = [System.Collections.Generic.HashSet[Guid]]::new()
function Add-EnabledFromRecords {
param([object[]]$Records)
foreach ($r in $Records) {
if (-not $r.Disabled) { [void]$enabledSet.Add([Guid]$r.GpoId) }
}
}
# Domain root links
$domain = Get-ADDomain -Server $pdc
$domainObj = Get-ADObject -Server $pdc -Identity $domain.DistinguishedName -Properties gPLink
Add-EnabledFromRecords (Get-GpLinkEntries -GpLink $domainObj.gPLink -TargetDn $domainObj.DistinguishedName -TargetType "Domain")
# OU links
Get-ADOrganizationalUnit -Server $pdc -Filter * -Properties gPLink | ForEach-Object {
Add-EnabledFromRecords (Get-GpLinkEntries -GpLink $_.gPLink -TargetDn $_.DistinguishedName -TargetType "OU")
}
# Site links
Get-ADReplicationSite -Server $pdc -Filter * | ForEach-Object {
$siteObj = Get-ADObject -Server $pdc -Identity $_.DistinguishedName -Properties gPLink
Add-EnabledFromRecords (Get-GpLinkEntries -GpLink $siteObj.gPLink -TargetDn $_.DistinguishedName -TargetType "Site")
}
# Count Apply principals (literal)
function Get-ApplyTrusteeCount {
param([Guid]$GpoId)
$apply = @(
Get-GPPermission -Guid $GpoId -All -Server $pdc -ErrorAction Stop |
Where-Object { $_.Permission -eq "GpoApply" }
)
$apply.Count
}
# Evaluate GPOs
$allGpos = Get-GPO -All -Server $pdc
$results = foreach ($gpo in $allGpos) {
$id = [Guid]$gpo.Id
$hasEnabledLink = $enabledSet.Contains($id)
$noEnabledLinks = -not $hasEnabledLink
$applyCount = $null
try { $applyCount = Get-ApplyTrusteeCount -GpoId $id } catch { $applyCount = $null }
$noSecurityFiltering = ($applyCount -eq 0)
$allSettingsDisabled = ($gpo.GpoStatus -eq "AllSettingsDisabled")
if ($noSecurityFiltering -or $noEnabledLinks -or $allSettingsDisabled) {
$reasons = @()
if ($noSecurityFiltering) { $reasons += "No Apply principals" }
if ($noEnabledLinks) { $reasons += "No enabled links (Domain/OU/Site) on PDC" }
if ($allSettingsDisabled) { $reasons += "All settings disabled" }
if ($applyCount -eq $null){ $reasons += "Apply check failed" }
[pscustomobject]@{
DisplayName = $gpo.DisplayName
Id = $gpo.Id
Status = $gpo.GpoStatus
ApplyCount = $applyCount
HasEnabledLinkOnPDC = $hasEnabledLink
Reasons = ($reasons -join "; ")
}
}
}
$results | Sort-Object DisplayName | Format-Table -AutoSize
The script provides a reliable way to find GPOs that are not really doing anything, making it a good starting point for any GPO cleanup project, and will help you keep your Group Policy infrastructure tidy and maintainable.