Cleaning Up the Mess in Your Group Policy (GPO) Environment

Intro

Group Policy is a great way to enforce policies and set preferences for any user or computer in your organization.
However, anyone who managed Group Policy knows it might become very messy after some time, especially if there are a lot of administrators who manage the Group Policy Objects (GPOs) in the company.

In this blog post series, we will cover some useful scripts and methods which will help you to organize and maintain your GPOs, and clean up the mess surrounded in your Group Policy environment.

First Things First – Create a backup

Before removing and modifying any Group Policy Object, It is highly recommended to create a backup of the current state of your Group Policy Objects.
This can be done using the Group Policy Management Console MMC, or by using the PowerShell cmdlet “Backup-GPO”.
To back up all GPOs, run the following PowerShell command:

Backup-GPO -All -Path "C:\Backup\GPO"

You can also create a scheduled task to back up Group Policy on a daily/weekly basis.
Use the following script to automatically create the backup schedule task for you:

Function Create-GPScheduleBackup
{
    $Message = "Please enter the credentials of the user which will run the schedule task"; 
    $Credential = $Host.UI.PromptForCredential("Please enter username and password",$Message,"$env:userdomain\$env:username",$env:userdomain)
    $SchTaskUsername = $credential.UserName
    $SchTaskPassword = $credential.GetNetworkCredential().Password
    $SchTaskScriptCode = '$Date = Get-Date -Format "yyyy-MM-dd_hh-mm"
    $BackupDir = "C:\Backup\GPO\$Date"
    $BackupRootDir = "C:\Backup\GPO"
    if (-Not (Test-Path -Path $BackupDir)) {
        New-Item -ItemType Directory -Path $BackupDir
    }
    $ErrorActionPreference = "SilentlyContinue" 
    Get-ChildItem $BackupRootDir | Where-Object {$_.CreationTime -le (Get-Date).AddMonths(-3)} | Foreach-Object { Remove-Item $_.FullName -Recurse -Force}
    Backup-GPO -All -Path $BackupDir'
    $SchTaskScriptFolder = "C:\Scripts\GPO"
    $SchTaskScriptPath = "C:\Scripts\GPO\GPOBackup.ps1"
    if (-Not (Test-Path -Path $SchTaskScriptFolder)) {
        New-Item -ItemType Directory -Path $SchTaskScriptFolder
    }
    if (-Not (Test-Path -Path $SchTaskScriptPath)) {
        New-Item -ItemType File -Path $SchTaskScriptPath
    }
    $SchTaskScriptCode | Out-File $SchTaskScriptPath
    $SchTaskAction = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument "-ExecutionPolicy Bypass $SchTaskScriptPath"
    $Frequency = "Daily","Weekly"
    $SelectedFrequnecy = $Frequency | Out-GridView -OutputMode Single -Title "Please select the required frequency"
    Switch ($SelectedFrequnecy) {
        Daily {
            $SchTaskTrigger =  New-ScheduledTaskTrigger -Daily -At 1am
        }
        Weekly {
            $Days = "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
            $SelectedDays = $Days | Out-GridView -OutputMode Multiple -Title "Please select the relevant days in which the schedule task will run"
            $SchTaskTrigger =  New-ScheduledTaskTrigger -Weekly -DaysOfWeek $SelectedDays -At 1am
        }
    }  
    Try {
        Register-ScheduledTask -Action $SchTaskAction -Trigger $SchTaskTrigger -TaskName "Group Policy Schedule Backup" -Description "Group Policy $SelectedFrequnecy Backup" -User $SchTaskUsername -Password $SchTaskPassword -RunLevel Highest -ErrorAction Stop
    }
    Catch {
        $ErrorMessage = $_.Exception.Message
        Write-Host "Schedule Task regisration was failed due to the following error: $ErrorMessage" -f Red
    }
}

Step 2 – Get Rid of Useless GPOs

There are probably a lot of useless GPOs in your Group Policy environment.
By useless, I mean Group Policies that are empty, disabled or not linked to any Organizational Unit (OU).

Each of the PowerShell functions below will create a report (Gird-View) with the affected GPOs (Disabled, Empty and Not-Linked), and remove those GPOs if requested by the user.

Please pay attention that all scripts are using ‘ReadOnlyMode’ parameter, which is set to ‘True’ by default to prevent any unwelcome changes and modifications on your environment.

Remove Disabled GPOs

Disabled GPOs are Group Policies which configured with GPO Status: “All Settings Disabled”, making it completely meaningless to computers and users policy. The following PowerShell script will identify those ‘Disabled’ Group Policies and provide you with the option to delete selected objects from your environment.

Function Get-GPDisabledGPOs ($ReadOnlyMode = $True) {
    ""
    "Looking for disabled GPOs..."
    $DisabledGPOs = @()
    Get-GPO -All | ForEach-Object {
        if ($_.GpoStatus -eq "AllSettingsDisabled") {
            Write-Host "Group Policy " -NoNewline; Write-Host $_.DisplayName -f Yellow -NoNewline; Write-Host " is configured with 'All Settings Disabled'"
            $DisabledGPOs += $_
        }
        Else {
            Write-Host "Group Policy " -NoNewline; Write-Host $_.DisplayName -f Green -NoNewline; Write-Host " is enabled"         
        }
    }
    Write-Host "Total GPOs with 'All Settings Disabled': $($DisabledGPOs.Count)" -f Yellow
    $GPOsToRemove = $DisabledGPOs | Select Id,DisplayName,ModificationTime,GpoStatus | Out-GridView -Title "Showing disabled Group Policies. Select GPOs you would like to delete" -OutputMode Multiple
    if ($ReadOnlyMode -eq $False -and $GPOsToRemove) {
        $GPOsToRemove | ForEach-Object {Remove-GPO -Guid $_.Id -Verbose}
    }
    if ($ReadOnlyMode -eq $True -and $GPOsToRemove) {
       Write-Host "Read-Only mode in enabled. Change 'ReadOnlyMode' parameter to 'False' in order to allow the script make changes" -ForegroundColor Red 
    }
}


Remove Unlinked GPOs

Group Policies can be linked to an AD Site, to a specific OU or the domain level.
Unlinked GPOs are Group Policies that are not linked to any of the above, and therefore have zero effect on computers and users on the domain. The following PowerShell script will identify those ‘Unlinked’ Group Policies and provide you with the option to delete selected objects from your environment.

Function Get-GPUnlinkedGPOs ($ReadOnlyMode = $True) { 
    ""
    "Looking for unlinked GPOs..."
    $UnlinkedGPOs = @()
    Get-GPO -All | ForEach-Object {
        If ($_ |Get-GPOReport -ReportType XML | Select-String -NotMatch "<LinksTo>" ) {
            Write-Host "Group Policy " -NoNewline; Write-Host $_.DisplayName -f Yellow -NoNewline; Write-Host " is not linked to any object (OU/Site/Domain)"
            $UnlinkedGPOs += $_
        }
        Else {
            Write-Host "Group Policy " -NoNewline; Write-Host $_.DisplayName -f Green -NoNewline; Write-Host " is linked"         
        }
    }
    Write-Host "Total of unlinked GPOs: $($UnlinkedGPOs.Count)" -f Yellow
    $GPOsToRemove = $UnlinkedGPOs | Select Id,DisplayName,ModificationTime | Out-GridView -Title "Showing unlinked Group Policies. Select GPOs you would like to delete" -OutputMode Multiple
    if ($ReadOnlyMode -eq $False -and $GPOsToRemove) {
        $GPOsToRemove | ForEach-Object {Remove-GPO -Guid $_.Id -Verbose}
    }
    if ($ReadOnlyMode -eq $True -and $GPOsToRemove) {
       Write-Host "Read-Only mode in enabled. Change 'ReadOnlyMode' parameter to 'False' in order to allow the script make changes" -ForegroundColor Red 
    }
}


Remove Empty GPOs

Empty GPO is a Group Policy Object which does not contain any settings.
An empty Group Policy can be identified using the User/Computer version of the GPO (when they are both equal to ‘0’), or when the Group Policy Report extension data is NULL.

The following PowerShell script will identify ‘Empty’ Group Policies using the methods described above, and provide you with the option to delete selected objects from your environment.

Function Get-GPEmptyGPOs ($ReadOnlyMode = $True) {
    ""
    "Looking for empty GPOs..."
    $EmptyGPOs = @()
    Get-GPO -All | ForEach-Object {
        $IsEmpty = $False
        If ($_.User.DSVersion -eq 0 -and $_.Computer.DSVersion -eq 0) {
            Write-Host "The Group Policy " -nonewline; Write-Host $_.DisplayName -f Yellow -NoNewline; Write-Host " is empty (no settings configured - User and Computer versions are both '0')"
            $EmptyGPOs += $_
            $IsEmpty = $True
        }
        Else {
            [xml]$Report = $_ | Get-GPOReport -ReportType Xml
            If ($Report.GPO.Computer.ExtensionData -eq $NULL -and $Report.GPO.User.ExtensionData -eq $NULL) {
                Write-Host "The Group Policy " -nonewline; Write-Host $_.DisplayName -f Yellow -NoNewline; Write-Host " is empty (no settings configured - No data exist)"
                $EmptyGPOs += $_
                $IsEmpty = $True
            }
        }
        If (-Not $IsEmpty) {
            Write-Host "Group Policy " -NoNewline; Write-Host $_.DisplayName -f Green -NoNewline; Write-Host " is not empty (contains data)"        
        }
    }
    Write-Host "Total of empty GPOs: $($EmptyGPOs.Count)" -f Yellow
    $GPOsToRemove = $EmptyGPOs | Select Id,DisplayName,ModificationTime | Out-GridView -Title "Showing empty Group Policies. Select GPOs you would like to delete" -OutputMode Multiple
    if ($ReadOnlyMode -eq $False -and $GPOsToRemove) {
        $GPOsToRemove | ForEach-Object {Remove-GPO -Guid $_.Id -Verbose}
    }
    if ($ReadOnlyMode -eq $True -and $GPOsToRemove) {
       Write-Host "Read-Only mode in enabled. Change 'ReadOnlyMode' parameter to 'False' in order to allow the script make changes" -ForegroundColor Red 
    }
}

In the next chapter, we will continue to review advanced methods and different ways of cleaning up Group Policy form unwanted GPOs. Stay tuned!