I often find myself opening the Group Policy Management Console, and wondering “are all of these GPOs actually being used?”. And if not, how do we go about finding GPOs that could be cleaned-up?
The script below is a little long, and though heavily commented, the summary of the logic is:
- Find all GPOs linked to every AD site with enabled links
- Find all GPOs linked to the root domain with enabled links
- Find all GPOs linked to every OU with enabled links
- Get a global list of every GPO and subtracts all of the above linked GPOs found
- Add to the list of candidates for deletion any GPO without any objects/permission within the Security Filtering area. (this is the bit that takes the longest)
- Add to the list of candidates for deletion any GPO with all settings disabled
And at the end of all that, we can display the names of the GPOs that are not in use and could be removed. EG:
There’s some logic in this that I’m not totally thrilled with (though it does work), where we try and find out if a GPO is link-enabled.
Specifically, raw GPO links look like this:
[LDAP://cn={293E490A-7769-41DD-9111-681ED01A91A9},cn=policies,cn=system,DC=kamal,DC=internal;0][LDAP://cn={74B6E272-2207-421C-9FB0-D306AD4E1098},cn=policies,cn=system,DC=kamal,DC=internal;0]
The string between the curly braces is the GUID of the GPO. The number following the semi-colon is the status of the link.
- 0 means link enabled, not enforced
- 1 means link disabled, enforced
- 2 means link enabled, enforced
- 3 means link disabled, not enforced
(we’re not concerned here with the enforced status, just if the link is enabled – options 0 and 2).
To extract out the GUID and the status, I used two different regex operations, which creates two parallel (loosely linked) lists, like this:
So, to find which GPOs are being used, we need to find the array index where the link option value is either 0 or 2, and use that same index number in the GUID array to find the correspodning GPO (this is why the $foreachcount variable, below, is in use). Again, I’m not thrilled with this method that I came up with – but it does work.
The script is also domain agnostic, and a copy/paste into your environment should work without modification. Here’s the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# Create array of a strings (strongly-typed) to hold currently linked/enabled GPO IDs [string[]]$appliedgpoids = @(); # Create array of a strings (strongly-typed) to hold all GPO IDs [string[]]$allgpoids = @(); # Create array of a strings (strongly-typed) to hold GPO IDs not in-use [string[]]$notinusegpoids = @(); # Define regex to find GUIDs of GPOs (the text between curly braces) $gpoidregex = "[^{\}]+(?=})"; # Define regex to find gpLink status (the text between ';' and ']') $gpolinkoptionsregex = "[^{;]+(?=\])"; # Get Distinguished Names $root = [ADSI]"LDAP://RootDSE" $domainName = ([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()).Name $domaindn = "DC=" + ($domainName -replace "\.", ",DC=") $sitesdn = "cn=sites," + ($root.configurationnamingcontext).tostring(); #----------------[ Get all site-linked GPOs ]---------------- # Get AD site $sites = get-adobject -ldapfilter "(objectclass=site)" -searchbase $sitesdn -properties gplink, gpoptions; # Loop through each site found foreach ($site in $sites) { # Get GPOs, by GUID, linked to the site $sitegpos = select-string -pattern $gpoidregex -input $site.gplink -allmatches; $sitegposlinkoptions = select-string -pattern $gpolinkoptionsregex -input $site.gplink -allmatches; # If linked GPOs are found if ($sitegpos -ne $null) { # Counter to track current item index $foreachcount = 0; # Loop through each linked GPO ID found and add to global in-use list foreach ($sitegpo in $sitegpos.matches) { # Check that the GPO link isn't disabled if ((($sitegposlinkoptions.matches)[$foreachcount].value -eq 0) -or (($sitegposlinkoptions.matches)[$foreachcount].value -eq 2)) { $appliedgpoids += $sitegpo.value; } # Increment counter $foreachcount++; } } } #----------------[ Get all domain-linked GPOs ]---------------- # Get domain info $domain = get-adobject $domaindn -properties gplink, gpoptions; # Get GPOs, by GUID, linked to the domain $domaingpos = select-string -pattern $gpoidregex -input $domain.gplink -allmatches; $domaingposlinkoptions = select-string -pattern $gpolinkoptionsregex -input $domain.gplink -allmatches; # If linked GPOs are found if ($domaingpos -ne $null) { # Counter to track current item index $foreachcount = 0; # Loop through each linked GPO ID found and add to global in-use list foreach ($domaingpo in $domaingpos.matches) { # Check that the GPO link isn't disabled if ((($domaingposlinkoptions.matches)[$foreachcount].value -eq 0) -or (($domaingposlinkoptions.matches)[$foreachcount].value -eq 2)) { $appliedgpoids += $domaingpo.value; } # Increment counter $foreachcount++; } } #----------------[ Get all OU-linked GPOs ]---------------- # Get all OUs $ous = get-adorganizationalunit -filter * -properties gplink, gpoptions; # Loop through each OU found foreach ($ou in $ous) { # Get GPOs, by GUID, linked to the OU $ougpos = select-string -pattern $gpoidregex -input $ou.gplink -allmatches; $ougposlinkoptions = select-string -pattern $gpolinkoptionsregex -input $ou.gplink -allmatches; if ($ougpos -ne $null) { # Counter to track current item index $foreachcount = 0; # Loop through each linked GPO ID found and add to global in-use list foreach ($ougpo in $ougpos.matches) { # Check that the GPO link isn't disabled if ((($ougposlinkoptions.matches)[$foreachcount].value -eq 0) -or (($ougposlinkoptions.matches)[$foreachcount].value -eq 2)) { $appliedgpoids += $ougpo.value; } # Increment counter $foreachcount++; } } } #----------------[ The rest ]---------------- # Remove duplicates from in-use GPO list $appliedgpoids = $appliedgpoids | select -unique; # Extract all GPOs $rawgpos = get-gpo -domain $domainName -all; # Loop through each GPO found foreach ($rawgpo in $rawgpos) { # Add ID for each GPO into array of strings (for later comparison) $allgpoids += $rawgpo.id; # Check Security Filtering (at least one setting with "GPOApply") $gpopermissions = get-gppermissions -all -guid $rawgpo.id.guid -DomainName $domainName | where {$_.permission -eq "gpoapply"}; # If a GPOApply permission is not found, add to master list of not in-use GPO IDs if ($gpopermissions -eq $null) { $notinusegpoids += $rawgpo.id.guid; } } # Calculate list of GPO IDs not in-use (all GPOs minus actively linked GPOs) $notinusegpoids += compare-object -referenceobject $allgpoids -differenceobject $appliedgpoids -passthru | where {$_.sideindicator -eq "<="}; # Find disabled GPOs and add to master list of not in-use GPO IDs $disabledgpos = $rawgpos | where {$_.gpostatus -eq "allsettingsdisabled"}; # Loop through each disabled GPO found and add to master list of not in-use GPO IDs foreach ($disabledgpo in $disabledgpos) { $notinusegpoids += $disabledgpo.id.guid; } # Remove duplicates from not in-use GPO IDs $notinusegpoids = $notinusegpoids | select -unique; # Get GPO info for each GPO ID not in use (display name, etc) $notinusegpos = $rawgpos | where {$_.id -in $notinusegpoids}; # Display GPOs not in-use, by Display Name $notinusegpos | sort displayname | select displayname; |