Finally! Copy and merge GPOs! PowerShell saves the day!

UPDATE

This script has been updated here.

 

The Problem

I wish I had this script five years ago. At the time I was searching for a way to combine or merge GPOs, but there simply wasn’t a way to do it. And today there still isn’t a way to do it… until POWERSHELL! Almost every environment I see (including my own former environment) has a collection of GPOs that have evolved over time and really need to be consolidated and cleaned up.

In Windows Server 2008 R2 we released a new PowerShell module for Group Policy administration. I haven’t seen much written about it, and I don’t think many people realize it is there.

I’ve been sitting on this script since last September, and ultimately I’d like to convert it to an advanced function. For now though I wanted to get this code published for the benefit of the community.

 

The Solution

You can use this PowerShell script to copy and merge GPOs. Yee haw! Essentially the script does a Get-GPRegistryValue from the source policy and a Set-GPRegistryValue on the destination policy. It recursively enumerates all settings in the source policy and copies them to the destination policy. But nothing is ever that simple. It turns out that there is a special trick to getting and setting a policy value that is in disabled status. After that hurdle it was smooth sailing. See the code for the details on this part.

 

Testing It For Yourself

You’ll need to run the script on Windows Server 2008 R2 or Windows 7 with the RSAT for Active Directory and Group Policy Management features installed. Here are the steps to test it in your environment:

1. In GPMC create a new GPO called "Starter Computer" based on one of the GPO computer templates.

2. In GPMC create a new GPO called "Starter User" based on one of the GPO user templates.

3. Run the script without switches for syntax: .\copy-gpregistryvalue.ps1

4. Run the script: .\copy-gpregistryvalue.ps1 -src "Starter Computer" -dest "Merged GPO" -newDest

5. Run the script: .\copy-gpregistryvalue.ps1 -src "Starter User" -dest "Merged GPO"

6. View the new "Merged GPO" settings in GPMC.

7. Compare these settings with the ones from the two source GPOs. They should match everything except the comments (which are unsupported by the current cmdlets).

This demo uses starter policies that separately contain user and computer settings, but you can use it just the same with existing source and destination policies containing both user and computer settings.

 

Limitations

Note that the Get/Set-GPRegistryValue cmdlets do not support the policy comments, therefore these will not be present in the destination GPO.

Also note that due to limitations of the GPO cmdlets we only copy registry-based settings in the policy. Other settings such as software installations, audit policies, file system permissions, etc. cannot be copied at this time.

Preference are not copied in the current version of the script. You could easily update the script to call similar cmdlets for Get/Set-GPPrefRegistryValue. I’ll leave that up to you.

 

Conclusion

Now go clean up all of those policies that need to be consolidated in your environment! (But first back them up with Backup-GPO.)

 

Resources

TechNet: Group Policy Cmdlets in Windows PowerShell

 

 #--------------------------------------------------------------------            
# Copy GPO Registry Settings            
# Ashley McGlone, Microsoft PFE            
# https://blogs.technet.com/b/ashleymcglone            
# January 2011            
#            
# Parameters:            
#   dom       FQDN of the domain where the GPOs reside            
#   src       string name of the GPO to copy settings from            
#   dest      string name of the GPO to copy settings to            
#   newDest   switch to create dest GPO if it does not exist            
#   copymode  part of GPO to copy: all, user, computer            
#--------------------------------------------------------------------            
            
Param (            
    $dom,            
    $src,            
    $dest,            
    [switch]$newDest,            
    $copymode            
)            
            
# We must continue on errors due to the way we enumerate GPO registry            
# paths and values in the function CopyValues.            
$ErrorActionPreference = "SilentlyContinue"            
$error.PSBase.Clear()            
            
Import-Module ActiveDirectory            
Import-Module GroupPolicy            
            
#--------------------------------------------------------------------            
# Help            
#--------------------------------------------------------------------            
if ($dom -eq $null -and `
    $src -eq $null -and `
    $dest -eq $null -and `
    $copymode -eq $null) {            
    ""            
    "Copy-GPORegistryValue by Ashley McGlone, Microsoft PFE"            
    "For more info: https://blogs.technet.com/b/ashleymcglone"            
    ""            
    "This script copies registry-based GPO settings from one GPO into another."            
    "Use this script to copy and/or merge policy settings."            
    "NOTE: This version does not copy GPO preferences."            
    ""            
    "Syntax:"            
    ".\Copy-GPRegistryValue.ps1 [-dom DomainFQDN] -src `"Source GPO`""            
    "   -dest `"Destination GPO`" [-newDest]"            
    "   [-copymode all/user/computer]"            
    ""            
    "The -dom switch will default to the current domain if blank."            
    "The -copymode will default to all if blank."            
    "The -newDest switch will create a new destination GPO of the specified"            
    "name. If the GPO already exists, then the copy will proceed."            
    ""            
    Return            
}            
            
#--------------------------------------------------------------------            
# Validate parameters            
#--------------------------------------------------------------------            
if ($dom -eq $null) {            
    $dom = (Get-ADDomain).DNSRoot            
} else {            
    $dom = (Get-ADDomain -Identity $dom).DNSRoot            
    If ($error.Count -ne 0) {            
        "Domain name does not exist.  Please specify a valid domain FQDN."            
        $error            
        Return            
    }            
}            
            
if ($src -eq $null) {            
    "Source GPO name cannot be blank."            
    Return            
} else {            
    $src = Get-GPO -Name $src            
    If ($error.Count -ne 0) {            
        "Source GPO does not exist.  Be sure to use quotes around the name."            
        Return            
    }            
}            
            
if ($dest -eq $null) {            
    "Destination GPO name cannot be blank."            
    Return            
} else {            
    if ($newDest -eq $true) {            
        $desttemp = $dest            
        $dest = New-GPO -Name $desttemp            
        If ($error.Count -ne 0) {            
            "The new destination GPO already exists."            
            "Do you want to merge into this GPO (y/n)?"            
            $choice = Read-Host            
            if ($choice -eq "y") {            
                $dest = Get-GPO -Name $desttemp            
            } else {            
                Return            
            }            
        }            
    } else {            
        $dest = Get-GPO -Name $dest            
        If ($error.Count -ne 0) {            
            "Destination GPO does not exist.  Be sure to use quotes around the name."            
            Return            
        }            
    }            
}            
            
if ($copymode -eq $null) {            
    $copymode = "all"            
} else {            
    if ($copymode -ne "all" -and `
        $copymode -ne "user" -and `
        $copymode -ne "computer") {            
        "copymode must be one of the following values:"            
        "all, user, computer"            
        Return            
    }            
}            
#--------------------------------------------------------------------            
            
            
#--------------------------------------------------------------------            
# Echo parameters for this run            
#--------------------------------------------------------------------            
""            
"Domain: $dom"            
"Source GPO: $($src.DisplayName)"            
"Destination GPO: $($dest.DisplayName)"            
"New Destination: $newDest"            
"CopyMode: $copymode"            
""            
#--------------------------------------------------------------------            
            
            
#--------------------------------------------------------------------            
# Copy GPO registry values recursively beginning at a specified root.            
#--------------------------------------------------------------------            
# THIS IS THE HEART OF THE SCRIPT.            
# Essentially this routine does a get from the source and a set on            
# the destination.  Of course nothing is ever that simple, so we have            
# to account for the policystate "delete" which disables a setting;            
# this is like a "negative set".            
# We recurse down each registry path until we find a value to            
# get/set.            
# If we try to get a value from a path (non-leaf level), then we get            
# an error and continue to dig down the path.  If we get a value and            
# no error, then we do the set.            
# User values have a single root: HKCU\Software.            
# Computer values have two roots: HKLM\System & HKLM\Software.            
# You can find these roots yourself by analyzing ADM and ADMX files.            
# It is normal to see an error in the output, because all of these            
# roots are not used in all policies.            
#--------------------------------------------------------------------            
Function CopyValues ($Key) {            
    $Key            
    $error.PSBase.Clear()            
    $path = Get-GPRegistryValue -GUID $src.ID -Key $Key            
    $path            
    If ($error.Count -eq 0) {            
        ForEach ($keypath in $path) {            
            $keypath            
            $keypath | ForEach-Object {Write-Host $_}            
            If ($keypath.HasValue) {            
                $keypath.PolicyState            
                $keypath.Valuename            
                $keypath.Type            
                $keypath.Value            
                If ($keypath.PolicyState -eq "Delete") {   # PolicyState = "Delete"            
                    Set-GPRegistryValue -Disable -Domain $dom -GUID $dest.ID `
                      -Key $keypath.FullKeyPath -ValueName $keypath.Valuename            
                } Else {   # PolicyState = "Set"            
                    $keypath | Set-GPRegistryValue -Domain $dom -GUID $dest.ID            
                }            
            } Else {            
                CopyValues $keypath.FullKeyPath            
            }            
        }            
    } Else {            
        $error            
    }            
}            
#--------------------------------------------------------------------            
            
            
#--------------------------------------------------------------------            
# Call the main copy routine for the specified scope of $copymode            
#--------------------------------------------------------------------            
Function Copy-GPRegistryValue {            
            
    # Copy user settings            
    If (($copymode -eq "user") -or ($copymode -eq "all")) {            
        CopyValues "HKCU\Software"            
        }            
            
    # Copy computer settings            
    If (($copymode -eq "computer") -or ($copymode -eq "all")) {            
        CopyValues "HKLM\System"            
        CopyValues "HKLM\Software"            
        }            
}            
#--------------------------------------------------------------------            
            
# Start the copy            
Copy-GPRegistryValue            
            
#   ><>

 

 

Copy-GPRegistryValue.p-s-1.txt