# GPOBackup.ps1 # GPO state backup and restore functions. # Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Update-GPOVersion) $Script:DefaultBackupRoot = Join-Path $PSScriptRoot '..\backups' $Script:DefaultRetention = 5 function Backup-GPOState { <# .SYNOPSIS Snapshots a GPO's SYSVOL directory tree and AD attributes to a timestamped backup directory. #> param( [Parameter(Mandatory)] [string]$GPOName, [string]$BackupRoot = $Script:DefaultBackupRoot, [string]$Domain = (Get-ADDomain).DNSRoot ) $gpo = Get-GPO -Name $GPOName -Domain $Domain -ErrorAction Stop $gpoGuid = "{$($gpo.Id)}" $domainDN = (Get-ADDomain -Server $Domain).DistinguishedName $gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$domainDN" $sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain # Create timestamped backup directory $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $safeName = $GPOName -replace '[^\w\-]', '_' $backupDir = Join-Path $BackupRoot "$safeName\$timestamp" if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null } # --- Snapshot SYSVOL --- # robocopy handles empty directories and UNC paths reliably; # Copy-Item -Recurse fails on empty dirs in PowerShell 5.1. $sysvolBackup = Join-Path $backupDir 'sysvol' if (Test-Path $sysvolPath) { $null = robocopy $sysvolPath $sysvolBackup /E /R:0 /W:0 /NJH /NJS /NFL /NDL /NP Write-Host " [BACKUP] SYSVOL files captured" -ForegroundColor Cyan } else { New-Item -ItemType Directory -Path $sysvolBackup -Force | Out-Null Write-Host " [BACKUP] No SYSVOL content to back up" -ForegroundColor DarkGray } # --- Snapshot AD attributes --- $adObj = Get-ADObject $gpoDN -Properties ` versionNumber, description, gPCMachineExtensionNames, ` gPCUserExtensionNames, gPCWQLFilter $metadata = @{ GPOName = $GPOName GPOGuid = $gpoGuid Timestamp = (Get-Date).ToString('o') Admin = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name Domain = $Domain VersionNumber = $adObj.versionNumber Description = $adObj.description gPCMachineExtensionNames = $adObj.gPCMachineExtensionNames gPCUserExtensionNames = $adObj.gPCUserExtensionNames gPCWQLFilter = $adObj.gPCWQLFilter } $metadataPath = Join-Path $backupDir 'metadata.json' $metadata | ConvertTo-Json -Depth 4 | Set-Content -Path $metadataPath -Encoding UTF8 Write-Host " [BACKUP] AD metadata captured -> $timestamp" -ForegroundColor Cyan # --- Enforce retention --- Invoke-GPOBackupRetention -GPOName $GPOName -BackupRoot $BackupRoot return $backupDir } function Invoke-GPOBackupRetention { <# .SYNOPSIS Removes old backups beyond the retention limit (default 5). #> param( [Parameter(Mandatory)] [string]$GPOName, [string]$BackupRoot = $Script:DefaultBackupRoot, [int]$RetentionCount = $Script:DefaultRetention ) $safeName = $GPOName -replace '[^\w\-]', '_' $gpoBackupDir = Join-Path $BackupRoot $safeName if (-not (Test-Path $gpoBackupDir)) { return } $backups = Get-ChildItem -Path $gpoBackupDir -Directory | Sort-Object Name -Descending if ($backups.Count -gt $RetentionCount) { $toRemove = $backups | Select-Object -Skip $RetentionCount foreach ($old in $toRemove) { Remove-Item -Path $old.FullName -Recurse -Force Write-Host " [BACKUP] Pruned old backup: $($old.Name)" -ForegroundColor DarkGray } } } function Get-GPOBackups { <# .SYNOPSIS Lists available backups for a GPO (or all GPOs if no name specified). #> param( [string]$GPOName, [string]$BackupRoot = $Script:DefaultBackupRoot ) if (-not (Test-Path $BackupRoot)) { return @() } $results = @() if ($GPOName) { $safeName = $GPOName -replace '[^\w\-]', '_' $searchPath = Join-Path $BackupRoot $safeName if (-not (Test-Path $searchPath)) { return @() } $gpoDirs = @(Get-Item $searchPath) } else { $gpoDirs = Get-ChildItem -Path $BackupRoot -Directory } foreach ($gpoDir in $gpoDirs) { $backups = Get-ChildItem -Path $gpoDir.FullName -Directory | Sort-Object Name -Descending foreach ($backup in $backups) { $metadataPath = Join-Path $backup.FullName 'metadata.json' if (Test-Path $metadataPath) { $meta = Get-Content $metadataPath -Raw | ConvertFrom-Json $results += [PSCustomObject]@{ GPOName = $meta.GPOName Timestamp = $backup.Name Admin = $meta.Admin Version = $meta.VersionNumber Path = $backup.FullName } } } } return $results } function Restore-GPOState { <# .SYNOPSIS Restores a GPO's SYSVOL files and AD attributes from a backup. .DESCRIPTION Copies backed-up SYSVOL content back to the live SYSVOL path and restores AD attributes (version, extensions, description, WMI filter link). Bumps the version number to force clients to reprocess. #> param( [Parameter(Mandatory)] [string]$BackupPath, [string]$Domain = (Get-ADDomain).DNSRoot ) $metadataPath = Join-Path $BackupPath 'metadata.json' if (-not (Test-Path $metadataPath)) { throw "Backup metadata not found at $BackupPath" } $meta = Get-Content $metadataPath -Raw | ConvertFrom-Json $gpoName = $meta.GPOName $gpo = Get-GPO -Name $gpoName -Domain $Domain -ErrorAction Stop $gpoGuid = "{$($gpo.Id)}" $domainDN = (Get-ADDomain -Server $Domain).DistinguishedName $gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$domainDN" $sysvolPath = Get-GPOSysvolPath -GPOName $gpoName -Domain $Domain Write-Host "Restoring GPO: $gpoName from backup $($meta.Timestamp)" -ForegroundColor Cyan # --- Restore SYSVOL --- $sysvolBackup = Join-Path $BackupPath 'sysvol' if (Test-Path $sysvolBackup) { $backedUpContent = Get-ChildItem $sysvolBackup if ($backedUpContent.Count -gt 0) { $null = robocopy $sysvolBackup $sysvolPath /E /R:0 /W:0 /NJH /NJS /NFL /NDL /NP Write-Host " [RESTORED] SYSVOL files" -ForegroundColor Green } else { Write-Host " [RESTORED] No SYSVOL content in backup (empty)" -ForegroundColor DarkGray } } # --- Restore AD attributes --- $replaceAttrs = @{} if ($meta.Description) { $replaceAttrs['description'] = $meta.Description } if ($meta.gPCMachineExtensionNames) { $replaceAttrs['gPCMachineExtensionNames'] = $meta.gPCMachineExtensionNames } if ($meta.gPCUserExtensionNames) { $replaceAttrs['gPCUserExtensionNames'] = $meta.gPCUserExtensionNames } if ($meta.gPCWQLFilter) { $replaceAttrs['gPCWQLFilter'] = $meta.gPCWQLFilter } if ($replaceAttrs.Count -gt 0) { Set-ADObject $gpoDN -Replace $replaceAttrs Write-Host " [RESTORED] AD attributes" -ForegroundColor Green } # --- Bump version to force client reprocessing --- Update-GPOVersion -GPOName $gpoName -Scope Both -Domain $Domain Write-Host " [RESTORED] Version bumped (clients will reprocess)" -ForegroundColor Green Write-Host "Restore complete for $gpoName." -ForegroundColor Green }