Guest post

This is a post written by my former colleague, Ofri Sherf.
I've been bugging her to upload her script and write how it works because it sounded interesting to me.

The story

I was asked to arrange Group Policy in one of our networks - e.g delete all unneccessary GPOs.
That sounded like it was going to be a tedious, frustrating, I-wanna-stab-my-neck-with-a-fork kind of work, so naturally I wanted to make my job a little bit easier by scripting it.

I divided my work into three stages:

1. Obvious cases

To find the following, first we need to retrieve all Group Policy Objects:

$GPOs = Get-GPO -All | select -exp ID | select -exp GUID | %{[xml](Get-GPOReport -Guid $_ -ReportType XML)} | select -exp GPO

I searched for GPOs (Group Policy objects) that are either:

  • Unlinked (a GPO that has no links in the AD, so it doesn't apply anywhere):

    $GPOs | ?{!($_.LinksTo)}
  • Empty (a GPO that contains no definition, so even if applied it contains nothing):

    $GPOs | ?{!($_.Computer.ExtensionData) -and !($_.User.ExtensionData)}
  • Both "Computer settings" and "User settings" segments are disabled:

    $GPOs | ?{!($_.Computer.Enabled) -and !($_.User.Enabled)}

2. Invalid "Administrative Templates" / "Windows Settings":

Here I targeted GPOs that have bad settings in either the "Administrative Templates" or "Windows Settings" segments. I have a script for this, but it's cumbersome and not fit for release. I might publish it after I clean it up.

3. Invalid Group Policy Preferences

The same invalid settings could be found in Group Policy Preference. However, preferences don't show up properly in the XML document generated by Get-GPOReport, so I didn't handle them in phase 2.
My script solved this issue by directly checking the XML files used to define these settings in the SYSVOL.
It produces a list of GPOs and the path to the file marking them as problematic by checking every XML and searching for something that looks like a path / AD SID, and checking it actually exists.

Note that if you're using a version of powershell < 3, you need to add these two lines to the start of the script:

Import-Module ActiveDirectory
Import-Module GroupPolicy

And here is the script:

# This script searches for GPOs who refer to something that does not exist
# Ofri Sherf

#region Params
# Domain to be checked. Defaults to the current domain
$domain = (Get-ADDomain | select -exp NetBIOSName)
# FQDN of domain to be checked
$fullDomain = (Get-ADDomain | select -exp DNSRoot)
# Policy segments to check. E.g. modify to "Machine" to avoid checking the user segment
$type = @("Machine","User") 

$ErrorActionPreference = "SilentlyContinue"

# Group Policy Preference to go over
$preferenceType = @{
    "Drives" = 'path="([^"]+)"';
    "Files" = 'fromPath="([^"]+)"';
    "Groups" = 'Member name="([^"]+)"';
    "NetworkShares" = 'path="([^"]+)"';
    "Shortcuts" = 'targetPath="([^"]+)"';
    "IniFiles" = 'path="([^"]+)"'

$preferenceType.GetEnumerator() | %{

    $currPreference = $_.Name
    $pattern = $_.Value

    # Analyze segment
    $type | %{
        $currType = $_
        # Find all GPOs
        ls \\$fullDomain\sysvol\$fullDomain\Policies | %{
            $setting = New-Object object
            $setting | Add-Member -MemberType NoteProperty -Name GPGuid ` 
            -Value ($_.Name -replace "\{|\}","")
            $setting | Add-Member -MemberType NoteProperty -Name GPName ` 
            -Value ($_.GPGuid | %{Get-GPO -Guid $_} | select -exp DisplayName)
            $setting | Add-Member -MemberType NoteProperty -Name Preference -Value ($currPreference)
            $setting | Add-Member -MemberType NoteProperty -Name Path -Value ""
            $GPName = $_.Name

        # Check if GPO contains relevant GPP
        if(Test-Path "\\$fullDomain\sysvol\$fullDomain\Policies\$GPName\$currType\Preferences\$currPreference\$currPreference.xml"){
            $file = Get-Content "\\$fullDomain\sysvol\$fullDomain\Policies\$GPName\$currType\Preferences\$currPreference\$currPreference.xml" -Encoding UTF8
            $file | %{
                # If GPP file matches the pattern of "external references"
                if($_ -match $pattern){
                        "Groups" { # This is a SID reference. Look for it in AD
                            $user = $null
                            $group = $null
                            if($Matches[1] -like "$domain*"){
                                $name = $Matches[1] -replace "$domain\\",""
                                $user = Get-ADUser $name
                                $group = Get-ADGroup $name
                                if(!($user) -and !($group)){
                                    $setting.path +=",$name"
                        "Shortcuts" { # This is a shortcut. It can point to a URL (http://)
                            if($_ -match 'targetPath="(http.+?)"'){
                                $web = Invoke-WebRequest $Matches[1] -UseDefaultCredentials
                                    $setting.path += "," +$Matches[1]
                            # If it's not HTTP, fall-through
                        default {
                            # This is a path
                            # Check if the path is a network path
                            if($Matches[1] -like "\\*"){
                                # Check if path is accessible
                                if(!(Test-Path $Matches[1])){
                                    # Check if path exists yet inaccessible
                                    if(!($Error[0].Exception -match "Access is denied")){
                                        $setting.path += "," + $Matches[1]

                    if($setting.path){$setting} # If the GPO has invalid settings, pipe it out