Damien Coles f172d00514 Initial release: Declarative AD Framework v2.1.0
Infrastructure-as-code framework for Active Directory objects and Group Policy.
Sanitized from production deployment for public sharing.
2026-02-19 17:02:42 +00:00

215 lines
7.6 KiB
PowerShell

# 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
}