Advanced Group Policy backup automation

Raj-GTSystems Engineer
CERTIFIED EXPERT
Published:
Updated:
In the absence of a fully-fledged GPO Management product like AGPM, the script in this article will provide you with a simple way to watch the domain (or a select OU) for GPOs changes and automatically take backups when policies are added, removed or changed with an option for email notifications.

A common problem with Group Policy change management is handling the backups.  It is not much of an issue when you have well-defined change processes or use an advance policy management product (like AGPM) but even then human error or corruption can make GPO changes potentially troublesome.


In this article, I am going to share with you a script I developed that may just save you from the above scenario by completely automating Group Policy backups.  When run on a recurring schedule (ideally every 3o mins), the script will watch the domain (or a select OU) for GPO changes and automatically take a backup of any new and changed policies.  The backup will include a human readable HTML (or XML) report of individual policies.  You can also have it send you a summary of changes via email if required.


The only prerequisites for the script are the commandlets supplied by Microsoft as part of Active Directory and Group Policy Management tools.  Without further ado, here is the fully commented codeā€¦


<#
.SYNOPSIS
    Watch for Group Policy changes under monitored OU (and child OUs) and take automatic backups and optionally, alert via e-mail

.DESCRIPTION
    When run (ideally on a recurring schedule via Task Scheduler) the script will check Group Policies linked under $watchedOU for changes and perform an automatic backup of just the changed policies. It will also generate individual HTML/XML reports of the changed policies and save it with the backups. You can also have a summary of changes sent to you via e-mail.

    Each set of backup is created under it's own folder and kept indefinitely.
   
.INPUTS
    None

.OUTPUTS
    None

.LINK
    https://github.com/Raj-GT/Windows-GroupPolicy-Monitor

.NOTES    
    Version:    1.4
    Author:     Nimal Raj
    Revisions:  19/07/2017      Initial draft (1.1)
                20/07/2017      Published in PowerShell Gallery (1.3)
                22/07/2017      Bug fix to enable backup of domain root policies (1.4)
#>

#Requires -Version 3.0

#---------------------------------------------------------[Modules]---------------------------------------------------------
Import-Module ActiveDirectory,GroupPolicy

#--------------------------------------------------------[Variables]--------------------------------------------------------
$watchedOU = "DC=CORP,DC=CONTOSO,DC=COM"            # Domain Root works as well
$rootDN = "DC=CORP,DC=CONTOSO,DC=COM"               # Required for our quick and dirty DN2Canonical function
$domainname = "CORP.CONTOSO.COM"                    # Required for our quick and dirty DN2Canonical function
$SMTP = "relay.contoso.com"                         # Assumes port TCP/25. Add -Port to Send-MailMessage if different
$mailFrom = "donotreply@contoso.com"                # From-address. For authenticated relays add -Credential to Send-MailMessage
$alertRecipient = "windows-admins@contoso.com"      # To-address. Leave empty to skip e-mail alerts
$scriptPath = $PSScriptRoot                         # Change the default backup path if required
$reportType = "HTML"                                # Valid options are HTML and XML
$backupFolder = "$scriptPath\Backups\" + (get-date -Format "yyyy-MM-ddThhmmss")

# E-mail template
$mailbody = @'
<style>
    body,p,h3 { font-family: calibri; }
    h3  { margin-bottom: 5px; }
    th  { text-align: center; background: #003829; color: #FFF; padding: 5px; }
    td  { padding: 5px 20px; }
    tr  { background: #E7FFF9; }
</style>

<p>GPO Monitor has detected the following changes...</p>

#mailcontent#

'@

# No user variables beyond this point
$ErrorActionPreference = "SilentlyContinue"
$GPCurrent = @()
$GPLast = $null
$reportbody = $null

#--------------------------------------------------------[Functions]--------------------------------------------------------
Function Backup ($GPO)
    {
        If ($GPO) {
            New-Item -Path $backupFolder -ItemType Directory;
            $GPO | Backup-GPO -Path $backupFolder;
            $GPO | ForEach-Object { Get-GPOReport -Name $_.PolicyName -ReportType $reportType -Path ("$backupFolder\"+$_.PolicyName+".$reportType") };
        }
    }
Function DN2Canon ($OUPath)
    {
        $Canon = $OUPath -Replace($rootDN,$domainname) -Replace("OU=","") -Split(",")
        [Array]::Reverse($Canon)
        $Canon = $Canon -Join "\"
        return($Canon)
    }

#--------------------------------------------------------[Execution]--------------------------------------------------------
# Generate list of GPOs linked under $watchedOU
$GPOLinks = (Get-ADOrganizationalUnit -SearchBase $watchedOU -Filter 'gpLink -gt "*"' | Get-GPInheritance).gpolinks

# Grab policies linked at domain root (in case $watchedOU is domain root)
If ($watchedOU.StartsWith("DC=")) {
    $GPOLinks += (Get-ADDomain | Get-GPInheritance).gpolinks
}

ForEach ($GPO in $GPOLinks) {
    $GPCurrent += New-Object -TypeName PSCustomObject -Property @{
    PolicyName  = (Get-GPO $GPO.GpoId).DisplayName;
    UpdateTime  = (Get-GPO $GPO.GpoId).ModificationTime;
    Enabled     = $GPO.Enabled;
    Guid        = $GPO.GpoId;
    OU          = DN2Canon($GPO.Target); 
    }
}

# Load the list of GPOs from last run for comparison
$GPLast = Import-Clixml -Path "$scriptPath\GPLast.xml"

# If no list is available then assume first run, create the list, backup all GPOs under $watchedOU and generate HTML/XML reports
If (!$GPLast -AND $GPCurrent) {
    $GPCurrent | Export-Clixml -Path "$scriptPath\GPLast.xml";
    Backup($GPCurrent)
}
Else {
# Let's compare the old list ($GPLast) to the current one ($GPCurrent)

    $GPList = $GPCurrent
    # Check for GPOs removed (guid missing from the current list)
    $RemovedGPO = Compare-Object $GPLast $GPList -Property Guid -PassThru | Where-Object {$_.SideIndicator -eq "<="}

    # Check for new GPOs (new guid in the current list)
    $NewGPO = Compare-Object $GPLast $GPList -Property Guid -PassThru | Where-Object {$_.SideIndicator -eq "=>"}
    # Remove the new GPO from the list before checking for changes (since new == change)
    $GPList = Compare-Object $GPList $NewGPO -Property Guid -PassThru

    # Check for changed GPOs
    $ChangedGPO = Compare-Object $GPLast $GPList -Property UpdateTime -PassThru | Where-Object {$_.SideIndicator -eq "=>"}

    # If anything has changed then create a backup (of new and changed GPOs), update GPLast.xml list and send -email
    If ($RemovedGPO -OR $NewGPO -OR $ChangedGPO) {
        $GPCurrent | Export-Clixml -Path "$scriptPath\GPLast.xml" -Force;
        Backup($NewGPO);
        Backup($ChangedGPO);
   
        # If $alertRecipient is not empty, then generate and send a summary of changes via e-mail
        If ($alertRecipient) {
            # Generate HTML tables for the report
            If ($NewGPO) { $reportbody += $NewGPO | ConvertTo-Html -Fragment -Property PolicyName,OU,UpdateTime -PreContent "<h3>Policies Added</h3>" }
            If ($ChangedGPO) { $reportbody += $ChangedGPO | ConvertTo-Html -Fragment -Property PolicyName,OU,UpdateTime -PreContent "<h3>Policies Updated</h3>" }
            If ($RemovedGPO) { $reportbody += $RemovedGPO | ConvertTo-Html -Fragment -Property PolicyName,OU,UpdateTime -PreContent "<h3>Policies Removed</h3>" }

            $mailbody = $mailbody.Replace("#mailcontent#",$reportbody)
            $mailbody = $mailbody.Replace("PolicyName","Policy Name")
            $mailbody = $mailbody.Replace("OU","Organizational Unit")
            $mailbody = $mailbody.Replace("UpdateTime","Update Time")

            Send-MailMessage -SmtpServer $SMTP -To $alertRecipient -From $mailFrom -Subject "Group Policy Monitor" -Body $mailbody -BodyAsHtml
        }
    }
}


The logic of the script should be pretty self-explanatory.  


Upon first run, the script will generate an xml report (GPLast.xml) of all policies linked under $watchedOU and any child OUs.  


It will also take a full backup of said policies at this point.  


On subsequent runs, the script will generate a list of policies in memory ($GPCurrent) and compare it to the xml from the last run.  It identifies new and removed policies by comparing the Guid property; changes are detected by comparing UpdateTime property.  


Once a change was identified, the list in memory is saved as the new GPLast.xml and a backup is performed for any new or changed policies.  


The last few lines of the code will simply convert the results into an HTML table, format it nicely and send it via e-mail.


I hope you enjoyed this little article and find my script useful.  I would greatly appreciate any comments and feedback on how we can improve the script further.


2
2,773 Views
Raj-GTSystems Engineer
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.