Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
215 lines
7.6 KiB
PowerShell
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
|
|
}
|