mirror of
https://github.com/NexusOne23/noid-privacy.git
synced 2026-02-07 12:11:53 +01:00
2969 lines
150 KiB
PowerShell
2969 lines
150 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Backup and rollback functionality for NoID Privacy Framework
|
|
|
|
.DESCRIPTION
|
|
Implements the BACKUP/APPLY/VERIFY/RESTORE pattern for safe system modifications.
|
|
Creates backups before changes and provides rollback capabilities.
|
|
|
|
.NOTES
|
|
Author: NexusOne23
|
|
Version: 2.2.0
|
|
Requires: PowerShell 5.1+
|
|
#>
|
|
|
|
# Global backup tracking (MUST be $global: for cross-module session sharing)
|
|
# Using $script: would create separate sessions per Import-Module call!
|
|
# IMPORTANT: Only initialize if not already set - prevents reset on re-load!
|
|
# NOTE: Must use Get-Variable to check existence (direct access fails in Strict Mode)
|
|
if (-not (Get-Variable -Name 'BackupIndex' -Scope Global -ErrorAction SilentlyContinue)) { $global:BackupIndex = @() }
|
|
if (-not (Get-Variable -Name 'BackupBasePath' -Scope Global -ErrorAction SilentlyContinue)) { $global:BackupBasePath = "" }
|
|
if (-not (Get-Variable -Name 'NewlyCreatedKeys' -Scope Global -ErrorAction SilentlyContinue)) { $global:NewlyCreatedKeys = @() }
|
|
if (-not (Get-Variable -Name 'SessionManifest' -Scope Global -ErrorAction SilentlyContinue)) { $global:SessionManifest = @{} }
|
|
if (-not (Get-Variable -Name 'CurrentModule' -Scope Global -ErrorAction SilentlyContinue)) { $global:CurrentModule = "" }
|
|
|
|
function Initialize-BackupSystem {
|
|
<#
|
|
.SYNOPSIS
|
|
Initialize the backup system
|
|
|
|
.PARAMETER BackupDirectory
|
|
Directory path for storing backups
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BackupDirectory = (Join-Path $PSScriptRoot "..\Backups")
|
|
)
|
|
|
|
# Create backup directory if it doesn't exist
|
|
if (-not (Test-Path -Path $BackupDirectory)) {
|
|
New-Item -ItemType Directory -Path $BackupDirectory -Force | Out-Null
|
|
}
|
|
|
|
# Reuse existing session if already initialized
|
|
if ($global:BackupBasePath -and (Test-Path -Path $global:BackupBasePath)) {
|
|
Write-Log -Level DEBUG -Message "Backup system already initialized, reusing session: $global:BackupBasePath" -Module "Rollback"
|
|
return $true
|
|
}
|
|
|
|
# Create session-specific backup folder
|
|
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$sessionId = "Session_$timestamp"
|
|
$sessionBackupPath = Join-Path $BackupDirectory $sessionId
|
|
New-Item -ItemType Directory -Path $sessionBackupPath -Force | Out-Null
|
|
|
|
# Normalize path for clean log output (removes ..\)
|
|
$global:BackupBasePath = [System.IO.Path]::GetFullPath($sessionBackupPath)
|
|
$global:BackupIndex = @()
|
|
$global:NewlyCreatedKeys = @()
|
|
|
|
# Initialize session manifest
|
|
$global:SessionManifest = @{
|
|
sessionId = $sessionId
|
|
displayName = "" # Auto-generated based on modules
|
|
sessionType = "unknown" # wizard | advanced | manual
|
|
timestamp = Get-Date -Format "o"
|
|
frameworkVersion = "2.2.0"
|
|
modules = @()
|
|
totalItems = 0
|
|
restorable = $true
|
|
sessionPath = $global:BackupBasePath
|
|
}
|
|
|
|
Write-Log -Level INFO -Message "Backup system initialized: $global:BackupBasePath" -Module "Rollback"
|
|
|
|
return $true
|
|
}
|
|
|
|
function Set-SessionType {
|
|
<#
|
|
.SYNOPSIS
|
|
Set the session type for better identification in restore UI
|
|
|
|
.PARAMETER SessionType
|
|
Type of session: wizard, advanced, or manual
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("wizard", "advanced", "manual")]
|
|
[string]$SessionType
|
|
)
|
|
|
|
if ($global:SessionManifest) {
|
|
$global:SessionManifest.sessionType = $SessionType
|
|
Write-Log -Level DEBUG -Message "Session type set to: $SessionType" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
function Update-SessionDisplayName {
|
|
<#
|
|
.SYNOPSIS
|
|
Auto-generate a user-friendly display name based on session type and modules
|
|
Should be called after all modules are backed up
|
|
#>
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
if (-not $global:SessionManifest) { return }
|
|
|
|
# Force arrays to prevent single-element string issues
|
|
$moduleCount = @($global:SessionManifest.modules).Count
|
|
$moduleNames = @($global:SessionManifest.modules | ForEach-Object { $_.name })
|
|
$sessionType = $global:SessionManifest.sessionType
|
|
|
|
# Calculate ACTUAL settings count (not backup items!)
|
|
# Each module applies a specific number of settings (Paranoid mode = max):
|
|
$settingsPerModule = @{
|
|
"SecurityBaseline" = 425 # 335 Registry + 67 Security Template + 23 Audit
|
|
"ASR" = 19 # 19 ASR Rules
|
|
"DNS" = 5 # 5 DNS Settings
|
|
"Privacy" = 77 # 53 Registry (MSRecommended) + 24 Bloatware
|
|
"AntiAI" = 32 # 32 Registry Policies (13 features incl. Copilot 4-Layer)
|
|
"EdgeHardening" = 24 # 24 Edge Policies (22-23 applied depending on extensions)
|
|
"AdvancedSecurity" = 50 # 50 Advanced Settings (15 features incl. Discovery Protocols + IPv6)
|
|
}
|
|
|
|
$totalSettings = 0
|
|
foreach ($moduleName in $moduleNames) {
|
|
if ($settingsPerModule.ContainsKey($moduleName)) {
|
|
$totalSettings += $settingsPerModule[$moduleName]
|
|
}
|
|
}
|
|
|
|
# Generate display name based on context
|
|
if ($moduleCount -eq 0) {
|
|
$displayName = "Empty Session"
|
|
}
|
|
elseif ($sessionType -eq "wizard") {
|
|
if ($moduleCount -ge 7) {
|
|
$displayName = "Full Hardening ($totalSettings Settings)"
|
|
}
|
|
elseif ($moduleCount -ge 4) {
|
|
$displayName = "Wizard: $moduleCount Modules ($totalSettings Settings)"
|
|
}
|
|
else {
|
|
# Few modules - list them
|
|
$short = ($moduleNames | Select-Object -First 3) -join ", "
|
|
$displayName = "Wizard: $short ($totalSettings Settings)"
|
|
}
|
|
}
|
|
elseif ($sessionType -eq "advanced") {
|
|
# Advanced mode = always single module
|
|
$displayName = "$($moduleNames[0]) Only ($totalSettings Settings)"
|
|
}
|
|
else {
|
|
# manual or unknown - just list modules
|
|
$short = ($moduleNames | Select-Object -First 2) -join ", "
|
|
if ($moduleCount -gt 2) { $short += "..." }
|
|
$displayName = "$short ($totalSettings Settings)"
|
|
}
|
|
|
|
$global:SessionManifest.displayName = $displayName
|
|
Write-Log -Level INFO -Message "Session display name: $displayName" -Module "Rollback"
|
|
|
|
# Update manifest file
|
|
$manifestPath = Join-Path $global:BackupBasePath "manifest.json"
|
|
if (Test-Path $manifestPath) {
|
|
try {
|
|
$encoding = New-Object System.Text.UTF8Encoding($false)
|
|
$json = $global:SessionManifest | ConvertTo-Json -Depth 5
|
|
[System.IO.File]::WriteAllText($manifestPath, $json, $encoding)
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to update manifest with display name: $_" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Start-ModuleBackup {
|
|
<#
|
|
.SYNOPSIS
|
|
Start backup for a specific module
|
|
|
|
.PARAMETER ModuleName
|
|
Name of the module (e.g., SecurityBaseline, ASR)
|
|
|
|
.OUTPUTS
|
|
String - Path to the module backup folder
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([string])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("SecurityBaseline", "ASR", "DNS", "Privacy", "AntiAI", "EdgeHardening", "AdvancedSecurity")]
|
|
[string]$ModuleName
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
# Create module subfolder
|
|
$moduleBackupPath = Join-Path $global:BackupBasePath $ModuleName
|
|
if (-not (Test-Path $moduleBackupPath)) {
|
|
New-Item -ItemType Directory -Path $moduleBackupPath -Force | Out-Null
|
|
}
|
|
|
|
$global:CurrentModule = $ModuleName
|
|
|
|
Write-Log -Level INFO -Message "Started backup for module: $ModuleName" -Module "Rollback"
|
|
|
|
# Return the module backup path
|
|
return $moduleBackupPath
|
|
}
|
|
|
|
function Complete-ModuleBackup {
|
|
<#
|
|
.SYNOPSIS
|
|
Complete backup for a module and update session manifest
|
|
|
|
.DESCRIPTION
|
|
Finalizes the backup process for the current module.
|
|
Updates the session manifest.json with module statistics.
|
|
This is CRITICAL for the Restore-Session function to work.
|
|
|
|
.PARAMETER ItemsBackedUp
|
|
Number of items successfully backed up
|
|
|
|
.PARAMETER Status
|
|
Status of the backup (Success, Failed, Skipped)
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$ItemsBackedUp,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("Success", "Failed", "Skipped")]
|
|
[string]$Status
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
if ([string]::IsNullOrEmpty($global:CurrentModule)) {
|
|
Write-Log -Level WARNING -Message "No active module backup to complete" -Module "Rollback"
|
|
return
|
|
}
|
|
|
|
# Update Manifest Object
|
|
$moduleData = @{
|
|
name = $global:CurrentModule
|
|
backupPath = $global:CurrentModule
|
|
itemsBackedUp = $ItemsBackedUp
|
|
status = $Status
|
|
timestamp = Get-Date -Format "o"
|
|
}
|
|
|
|
$global:SessionManifest.modules += $moduleData
|
|
$global:SessionManifest.totalItems += $ItemsBackedUp
|
|
|
|
# Write Manifest to Disk (robust against transient file locks)
|
|
$manifestPath = Join-Path $global:BackupBasePath "manifest.json"
|
|
$maxAttempts = 5
|
|
$attempt = 0
|
|
$delayMs = 200
|
|
$encoding = New-Object System.Text.UTF8Encoding($false)
|
|
|
|
while ($attempt -lt $maxAttempts) {
|
|
try {
|
|
$attempt++
|
|
$json = $global:SessionManifest | ConvertTo-Json -Depth 5
|
|
[System.IO.File]::WriteAllText($manifestPath, $json, $encoding)
|
|
Write-Log -Level INFO -Message "Completed backup for $($global:CurrentModule) (Items: $ItemsBackedUp). Manifest updated." -Module "Rollback"
|
|
break
|
|
}
|
|
catch [System.IO.IOException] {
|
|
if ($attempt -ge $maxAttempts) {
|
|
Write-Log -Level ERROR -Message "Failed to write session manifest after $maxAttempts attempts: $_" -Module "Rollback"
|
|
break
|
|
}
|
|
Start-Sleep -Milliseconds $delayMs
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to write session manifest: $_" -Module "Rollback"
|
|
break
|
|
}
|
|
}
|
|
|
|
# Reset Current Module
|
|
$global:CurrentModule = ""
|
|
}
|
|
|
|
function Backup-RegistryKey {
|
|
<#
|
|
.SYNOPSIS
|
|
Backup a registry key before modification
|
|
|
|
.PARAMETER KeyPath
|
|
Registry key path (e.g., "HKLM:\SOFTWARE\Policies\Microsoft\Windows")
|
|
|
|
.PARAMETER BackupName
|
|
Descriptive name for this backup
|
|
|
|
.OUTPUTS
|
|
String containing backup file path
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([string])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$KeyPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BackupName
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
try {
|
|
# Sanitize backup name for filename
|
|
$safeBackupName = $BackupName -replace '[\\/:*?"<>|]', '_'
|
|
|
|
# Save to current module folder if active, otherwise root
|
|
$backupFolder = if ($global:CurrentModule) {
|
|
Join-Path $global:BackupBasePath $global:CurrentModule
|
|
}
|
|
else {
|
|
$global:BackupBasePath
|
|
}
|
|
|
|
$backupFile = Join-Path $backupFolder "$safeBackupName`_Registry.reg"
|
|
|
|
# Convert PowerShell path to reg.exe format
|
|
$regPath = $KeyPath -replace 'HKLM:\\', 'HKEY_LOCAL_MACHINE\' `
|
|
-replace 'HKCU:\\', 'HKEY_CURRENT_USER\' `
|
|
-replace 'HKCR:\\', 'HKEY_CLASSES_ROOT\' `
|
|
-replace 'HKU:\\', 'HKEY_USERS\' `
|
|
-replace 'HKCC:\\', 'HKEY_CURRENT_CONFIG\'
|
|
|
|
# Use unique temp files to prevent race conditions
|
|
$guid = [Guid]::NewGuid().ToString()
|
|
$stdoutFile = Join-Path $env:TEMP "reg_export_stdout_$guid.txt"
|
|
$stderrFile = Join-Path $env:TEMP "reg_export_stderr_$guid.txt"
|
|
|
|
# Export registry key using Start-Process for better error handling
|
|
$process = Start-Process -FilePath "reg.exe" `
|
|
-ArgumentList "export", "`"$regPath`"", "`"$backupFile`"", "/y" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru `
|
|
-RedirectStandardOutput $stdoutFile `
|
|
-RedirectStandardError $stderrFile
|
|
|
|
# Cleanup temp files
|
|
$errorOutput = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
|
|
Remove-Item $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue
|
|
|
|
if ($process.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Registry backup created: $BackupName" -Module "Rollback"
|
|
|
|
# Add to backup index
|
|
$global:BackupIndex += [PSCustomObject]@{
|
|
Type = "Registry"
|
|
Name = $BackupName
|
|
Path = $KeyPath
|
|
BackupFile = $backupFile
|
|
Timestamp = Get-Date
|
|
}
|
|
|
|
return $backupFile
|
|
}
|
|
else {
|
|
# Check if key simply doesn't exist yet (normal when creating new keys)
|
|
if ($errorOutput -match "nicht gefunden|cannot find|not found") {
|
|
# Key doesn't exist - CREATE EMPTY MARKER so restore knows to DELETE this key
|
|
Write-Log -Level INFO -Message "Registry key does not exist (will create empty marker): $BackupName" -Module "Rollback"
|
|
|
|
try {
|
|
$emptyMarker = @{
|
|
KeyPath = $KeyPath
|
|
BackupDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
State = "NotExisted"
|
|
Message = "Registry key did not exist before hardening - must be deleted during restore"
|
|
} | ConvertTo-Json
|
|
|
|
$markerFile = Join-Path $backupFolder "$safeBackupName`_EMPTY.json"
|
|
$emptyMarker | Set-Content -Path $markerFile -Encoding UTF8 -Force
|
|
|
|
Write-Log -Level SUCCESS -Message "Empty marker created for non-existent key: $BackupName" -Module "Rollback"
|
|
|
|
# Add to backup index
|
|
$global:BackupIndex += [PSCustomObject]@{
|
|
Type = "EmptyMarker"
|
|
Name = $BackupName
|
|
Path = $KeyPath
|
|
BackupFile = $markerFile
|
|
Timestamp = Get-Date
|
|
}
|
|
|
|
return $markerFile
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not create empty marker for ${BackupName}: $($_.Exception.Message)" -Module "Rollback"
|
|
return $null
|
|
}
|
|
}
|
|
else {
|
|
# Actual error
|
|
Write-Log -Level WARNING -Message "Registry backup may have failed: $errorOutput" -Module "Rollback"
|
|
return $null
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to backup registry key: $KeyPath" -Module "Rollback" -ErrorRecord $_
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Register-NewRegistryKey {
|
|
<#
|
|
.SYNOPSIS
|
|
Track a newly created registry key for proper restore
|
|
|
|
.DESCRIPTION
|
|
When a registry key is created that didn't exist before, it must be tracked
|
|
so it can be deleted (not just restored) during rollback.
|
|
|
|
.PARAMETER KeyPath
|
|
PowerShell-style registry path (e.g., HKLM:\SOFTWARE\...)
|
|
|
|
.EXAMPLE
|
|
Register-NewRegistryKey -KeyPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\NewKey"
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$KeyPath
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
# Add to tracking list (avoid duplicates)
|
|
if ($global:NewlyCreatedKeys -notcontains $KeyPath) {
|
|
$global:NewlyCreatedKeys += $KeyPath
|
|
Write-Log -Level DEBUG -Message "Tracking new registry key for rollback: $KeyPath" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
function Backup-ServiceConfiguration {
|
|
<#
|
|
.SYNOPSIS
|
|
Backup service configuration before modification
|
|
|
|
.PARAMETER ServiceName
|
|
Name of the service
|
|
|
|
.PARAMETER BackupName
|
|
Optional descriptive name for this backup. If not provided, uses ServiceName.
|
|
|
|
.OUTPUTS
|
|
String containing backup file path
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([string])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServiceName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BackupName
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
# Use ServiceName as BackupName if not provided
|
|
if ([string]::IsNullOrEmpty($BackupName)) {
|
|
$BackupName = $ServiceName
|
|
}
|
|
|
|
try {
|
|
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
|
|
|
# Get detailed service configuration (may not exist for some services)
|
|
$serviceConfig = Get-CimInstance -ClassName Win32_Service -Filter "Name='$ServiceName'" -ErrorAction SilentlyContinue
|
|
|
|
$backupData = [PSCustomObject]@{
|
|
Name = $service.Name
|
|
DisplayName = $service.DisplayName
|
|
Status = $service.Status
|
|
StartType = $service.StartType
|
|
StartMode = if ($serviceConfig) { $serviceConfig.StartMode } else { $service.StartType.ToString() }
|
|
PathName = if ($serviceConfig) { $serviceConfig.PathName } else { "" }
|
|
Description = if ($serviceConfig) { $serviceConfig.Description } else { "" }
|
|
}
|
|
|
|
# Save to JSON
|
|
$safeBackupName = $BackupName -replace '[\\/:*?"<>|]', '_'
|
|
|
|
# Save to current module folder if active, otherwise root
|
|
$backupFolder = if ($global:CurrentModule) {
|
|
Join-Path $global:BackupBasePath $global:CurrentModule
|
|
}
|
|
else {
|
|
$global:BackupBasePath
|
|
}
|
|
|
|
$backupFile = Join-Path $backupFolder "$safeBackupName`_Service.json"
|
|
$backupData | ConvertTo-Json | Set-Content -Path $backupFile -Encoding UTF8 | Out-Null
|
|
|
|
Write-Log -Level SUCCESS -Message "Service backup created: $BackupName ($ServiceName)" -Module "Rollback"
|
|
|
|
# Add to backup index
|
|
$global:BackupIndex += [PSCustomObject]@{
|
|
Type = "Service"
|
|
Name = $BackupName
|
|
ServiceName = $ServiceName
|
|
BackupFile = $backupFile
|
|
Timestamp = Get-Date
|
|
}
|
|
|
|
return $backupFile
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to backup service: $ServiceName" -Module "Rollback" -Exception $_.Exception
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Backup-ScheduledTask {
|
|
<#
|
|
.SYNOPSIS
|
|
Backup scheduled task configuration before modification
|
|
|
|
.PARAMETER TaskPath
|
|
Full path of the scheduled task (e.g., "\Microsoft\Windows\AppID\TaskName")
|
|
Can be either full path or just folder path if TaskName is provided separately.
|
|
|
|
.PARAMETER TaskName
|
|
Optional - Name of the scheduled task if TaskPath is just the folder
|
|
|
|
.PARAMETER BackupName
|
|
Optional descriptive name for this backup. Auto-generated if not provided.
|
|
|
|
.OUTPUTS
|
|
String containing backup file path
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([string])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TaskPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$TaskName,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BackupName
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($global:BackupBasePath)) {
|
|
throw "Backup system not initialized. Call Initialize-BackupSystem first."
|
|
}
|
|
|
|
try {
|
|
# Parse TaskPath - if it contains task name, split it
|
|
if ([string]::IsNullOrEmpty($TaskName)) {
|
|
# TaskPath is full path like "\Microsoft\Windows\AppID\TaskName"
|
|
$TaskName = Split-Path $TaskPath -Leaf
|
|
$actualTaskPath = Split-Path $TaskPath -Parent
|
|
if ([string]::IsNullOrEmpty($actualTaskPath)) {
|
|
$actualTaskPath = "\"
|
|
}
|
|
}
|
|
else {
|
|
$actualTaskPath = $TaskPath
|
|
}
|
|
|
|
# Generate BackupName if not provided
|
|
if ([string]::IsNullOrEmpty($BackupName)) {
|
|
$BackupName = $TaskName -replace '\s', '_'
|
|
}
|
|
|
|
# Check if task exists first
|
|
$task = Get-ScheduledTask -TaskPath $actualTaskPath -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
|
|
if (-not $task) {
|
|
# Task doesn't exist - this is normal for many telemetry tasks on Win11
|
|
Write-Log -Level DEBUG -Message "Scheduled task not found (already disabled/removed): $actualTaskPath\$TaskName" -Module "Rollback"
|
|
return $null
|
|
}
|
|
|
|
# Export task to XML
|
|
$taskXml = Export-ScheduledTask -TaskPath $actualTaskPath -TaskName $TaskName
|
|
|
|
# Save to file
|
|
$safeBackupName = $BackupName -replace '[\\/:*?"<>|]', '_'
|
|
|
|
# Save to current module folder if active, otherwise root
|
|
$backupFolder = if ($global:CurrentModule) {
|
|
Join-Path $global:BackupBasePath $global:CurrentModule
|
|
}
|
|
else {
|
|
$global:BackupBasePath
|
|
}
|
|
|
|
$backupFile = Join-Path $backupFolder "$safeBackupName`_Task.xml"
|
|
$taskXml | Set-Content -Path $backupFile -Encoding UTF8 | Out-Null
|
|
|
|
Write-Log -Level SUCCESS -Message "Scheduled task backup created: $BackupName" -Module "Rollback"
|
|
|
|
# Add to backup index
|
|
$global:BackupIndex += [PSCustomObject]@{
|
|
Type = "ScheduledTask"
|
|
Name = $BackupName
|
|
TaskPath = $TaskPath
|
|
TaskName = $TaskName
|
|
BackupFile = $backupFile
|
|
Timestamp = Get-Date
|
|
}
|
|
|
|
return $backupFile
|
|
}
|
|
catch {
|
|
# Only log as ERROR if task exists but backup failed (real error)
|
|
Write-Log -Level ERROR -Message "Failed to backup scheduled task: $actualTaskPath\$TaskName" -Module "Rollback" -Exception $_.Exception
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Register-Backup {
|
|
<#
|
|
.SYNOPSIS
|
|
Register a generic backup with custom data
|
|
|
|
.DESCRIPTION
|
|
Allows modules to register custom backup data (e.g., DNS settings, firewall rules).
|
|
The data is stored as JSON and can be restored using module-specific restore logic.
|
|
|
|
.PARAMETER Type
|
|
Type of backup (e.g., "DNS", "Firewall", "Custom")
|
|
|
|
.PARAMETER Data
|
|
Backup data as JSON string or PowerShell object
|
|
|
|
.PARAMETER Name
|
|
Optional descriptive name for the backup
|
|
|
|
.OUTPUTS
|
|
Path to backup file or $null if failed
|
|
|
|
.EXAMPLE
|
|
Register-Backup -Type "DNS" -Data $dnsBackupJson -Name "DNS_Settings"
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([string])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Type,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
$Data,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Name
|
|
)
|
|
|
|
try {
|
|
if (-not $global:BackupBasePath) {
|
|
Write-Log -Level ERROR -Message "Backup system not initialized" -Module "Rollback"
|
|
return $null
|
|
}
|
|
|
|
# Generate backup name if not provided
|
|
if (-not $Name) {
|
|
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$Name = "$Type`_$timestamp"
|
|
}
|
|
|
|
# Sanitize backup name
|
|
$safeName = $Name -replace '[\\/:*?"<>|]', '_'
|
|
|
|
# Create type-specific folder
|
|
$typeFolder = Join-Path $global:BackupBasePath $Type
|
|
if (-not (Test-Path $typeFolder)) {
|
|
New-Item -ItemType Directory -Path $typeFolder -Force | Out-Null
|
|
}
|
|
|
|
$backupFile = Join-Path $typeFolder "$safeName.json"
|
|
|
|
# Convert data to JSON if not already
|
|
if ($Data -is [string]) {
|
|
$Data | Set-Content -Path $backupFile -Encoding UTF8 | Out-Null
|
|
}
|
|
else {
|
|
$Data | ConvertTo-Json -Depth 10 | Set-Content -Path $backupFile -Encoding UTF8 | Out-Null
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "Backup registered: $Type - $Name" -Module "Rollback"
|
|
|
|
# Add to backup index
|
|
$global:BackupIndex += [PSCustomObject]@{
|
|
Type = $Type
|
|
Name = $Name
|
|
BackupFile = $backupFile
|
|
Timestamp = Get-Date
|
|
}
|
|
|
|
return $backupFile
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to register backup: $Type - $Name" -Module "Rollback" -Exception $_.Exception
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function New-SystemRestorePoint {
|
|
<#
|
|
.SYNOPSIS
|
|
Create a system restore point
|
|
|
|
.PARAMETER Description
|
|
Description for the restore point
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Description = "NoID Privacy - Before Hardening"
|
|
)
|
|
|
|
try {
|
|
# Check if System Restore is enabled
|
|
$restoreEnabled = $null -ne (Get-ComputerRestorePoint -ErrorAction SilentlyContinue)
|
|
|
|
if ($restoreEnabled) {
|
|
Write-Log -Level INFO -Message "Creating system restore point..." -Module "Rollback"
|
|
|
|
Checkpoint-Computer -Description $Description -RestorePointType "MODIFY_SETTINGS"
|
|
|
|
Write-Log -Level SUCCESS -Message "System restore point created" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "System Restore is not enabled on this system" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to create system restore point" -Module "Rollback" -Exception $_.Exception
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Get-BackupIndex {
|
|
<#
|
|
.SYNOPSIS
|
|
Get list of all backups created in current session
|
|
|
|
.OUTPUTS
|
|
Array of backup objects
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([PSCustomObject[]])]
|
|
param()
|
|
|
|
return $global:BackupIndex
|
|
}
|
|
|
|
function Restore-FromBackup {
|
|
<#
|
|
.SYNOPSIS
|
|
Restore a specific backup
|
|
|
|
.PARAMETER BackupFile
|
|
Path to backup file
|
|
|
|
.PARAMETER Type
|
|
Type of backup (Registry, Service, ScheduledTask)
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BackupFile,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("Registry", "Service", "ScheduledTask")]
|
|
[string]$Type
|
|
)
|
|
|
|
if (-not (Test-Path -Path $BackupFile)) {
|
|
Write-Log -Level ERROR -Message "Backup file not found: $BackupFile" -Module "Rollback"
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
switch ($Type) {
|
|
"Registry" {
|
|
Write-Log -Level INFO -Message "Restoring registry from: $BackupFile" -Module "Rollback"
|
|
|
|
# Check if backup file has content (more than just header)
|
|
$backupContent = Get-Content -Path $BackupFile -Raw -ErrorAction SilentlyContinue
|
|
$hasContent = $backupContent -and ($backupContent.Length -gt 100) -and ($backupContent -match '\[HKEY')
|
|
|
|
if (-not $hasContent) {
|
|
# Backup is empty - the key didn't exist before hardening
|
|
# Extract key path from filename and delete it
|
|
Write-Log -Level INFO -Message "Empty backup detected - key did not exist before hardening" -Module "Rollback"
|
|
|
|
# Try to extract key path from backup content if available
|
|
if ($backupContent -match '\[HKEY[^\]]+\]') {
|
|
$keyPath = $matches[0] -replace '^\[' -replace '\]$'
|
|
|
|
# Use [regex]::Escape to prevent unintended matches
|
|
$keyPath = $keyPath -replace [regex]::Escape('HKEY_LOCAL_MACHINE'), 'HKLM:' `
|
|
-replace [regex]::Escape('HKEY_CURRENT_USER'), 'HKCU:' `
|
|
-replace [regex]::Escape('HKEY_CLASSES_ROOT'), 'HKCR:' `
|
|
-replace [regex]::Escape('HKEY_USERS'), 'HKU:' `
|
|
-replace [regex]::Escape('HKEY_CURRENT_CONFIG'), 'HKCC:'
|
|
|
|
# CRITICAL: Validate key path is within expected scope!
|
|
$allowedPrefixes = @(
|
|
'HKLM:\\SOFTWARE\\Policies',
|
|
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies',
|
|
'HKCU:\\SOFTWARE\\Policies',
|
|
'HKLM:\\SYSTEM\\CurrentControlSet\\Services',
|
|
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
|
|
'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server'
|
|
)
|
|
|
|
$isAllowed = $false
|
|
foreach ($prefix in $allowedPrefixes) {
|
|
if ($keyPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
$isAllowed = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $isAllowed) {
|
|
Write-Log -Level WARNING -Message "Refusing to delete key outside allowed scope: $keyPath" -Module "Rollback"
|
|
return $true
|
|
}
|
|
|
|
if (Test-Path $keyPath) {
|
|
try {
|
|
Remove-Item -Path $keyPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "Deleted non-existent key: $keyPath" -Module "Rollback"
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not delete key: $keyPath - $_" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Log -Level INFO -Message "Backup empty - nothing to restore" -Module "Rollback"
|
|
return $true
|
|
}
|
|
|
|
# PRE-CHECK: Extract key path from .reg file and check if it's a protected key
|
|
# This prevents unnecessary WARNING/ERROR messages for known protected keys
|
|
$keyPathToRestore = ""
|
|
$backupContent = Get-Content -Path $BackupFile -Raw -ErrorAction SilentlyContinue
|
|
if ($backupContent -match '\[(HKEY[^\]]+)\]') {
|
|
$keyPathToRestore = $matches[1]
|
|
}
|
|
|
|
# List of known protected keys (Windows system protection prevents reg.exe import)
|
|
$knownProtectedKeys = @(
|
|
'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server',
|
|
'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings',
|
|
'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
|
)
|
|
|
|
$isKnownProtected = $false
|
|
foreach ($protectedKey in $knownProtectedKeys) {
|
|
if ($keyPathToRestore -match [regex]::Escape($protectedKey)) {
|
|
$isKnownProtected = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
# If this is a known protected key, skip reg.exe import and use JSON-Fallback instead
|
|
if ($isKnownProtected) {
|
|
Write-Log -Level INFO -Message "Standard registry import skipped for protected key (will use Smart JSON-Fallback)." -Module "Rollback"
|
|
return $true # Success - JSON backup will handle this key via Smart Fallback
|
|
}
|
|
|
|
# Use unique temp files to prevent race conditions
|
|
$guid = [Guid]::NewGuid().ToString()
|
|
$stdoutFile = Join-Path $env:TEMP "reg_import_stdout_$guid.txt"
|
|
$stderrFile = Join-Path $env:TEMP "reg_import_stderr_$guid.txt"
|
|
|
|
# Use Start-Process to properly handle reg.exe output
|
|
$process = Start-Process -FilePath "reg.exe" `
|
|
-ArgumentList "import", "`"$BackupFile`"" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru `
|
|
-RedirectStandardOutput $stdoutFile `
|
|
-RedirectStandardError $stderrFile
|
|
|
|
# Cleanup temp files
|
|
$errorOutput = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
|
|
Remove-Item $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue
|
|
|
|
if ($process.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Registry restored successfully" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
$errorMessage = $errorOutput
|
|
# Check for Access Denied error (English and German variants)
|
|
if ($errorMessage -match "Zugriff verweigert|Access is denied|Fehler beim Zugriff auf die Registrierung") {
|
|
Write-Log -Level WARNING -Message "Access Denied during registry restore for $BackupFile. Attempting to delete key and retry import..." -Module "Rollback"
|
|
|
|
if (-not [string]::IsNullOrEmpty($keyPathToRestore)) {
|
|
try {
|
|
# Convert reg.exe path to PowerShell path
|
|
$psKeyPath = $keyPathToRestore -replace 'HKEY_LOCAL_MACHINE', 'HKLM:' `
|
|
-replace 'HKEY_CURRENT_USER', 'HKCU:' `
|
|
-replace 'HKEY_CLASSES_ROOT', 'HKCR:' `
|
|
-replace 'HKEY_USERS', 'HKU:' `
|
|
-replace 'HKEY_CURRENT_CONFIG', 'HKCC:'
|
|
|
|
if (Test-Path $psKeyPath) {
|
|
Write-Log -Level INFO -Message "Deleting existing protected key: $psKeyPath before re-import." -Module "Rollback"
|
|
Remove-Item -Path $psKeyPath -Recurse -Force -ErrorAction SilentlyContinue # SilentlyContinue to avoid error if it's truly protected
|
|
}
|
|
|
|
# Retry import
|
|
$process = Start-Process -FilePath "reg.exe" `
|
|
-ArgumentList "import", "`"$BackupFile`"" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru `
|
|
-RedirectStandardOutput $stdoutFile `
|
|
-RedirectStandardError $stderrFile
|
|
|
|
$errorOutput = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
|
|
Remove-Item $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue
|
|
|
|
if ($process.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Registry restored successfully after deleting key and retrying" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level ERROR -Message "Registry restore failed even after deleting key (Exit Code: $($process.ExitCode)): $errorOutput" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to delete key or retry import for ${keyPathToRestore}: $($_.Exception.Message)" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
}
|
|
Write-Log -Level ERROR -Message "Registry restore failed (Exit Code: $($process.ExitCode)): $errorMessage" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
"Service" {
|
|
Write-Log -Level INFO -Message "Restoring service from: $BackupFile" -Module "Rollback"
|
|
$serviceConfig = Get-Content -Path $BackupFile -Raw | ConvertFrom-Json
|
|
|
|
Set-Service -Name $serviceConfig.Name -StartupType $serviceConfig.StartType -ErrorAction Stop
|
|
|
|
Write-Log -Level SUCCESS -Message "Service restored: $($serviceConfig.Name)" -Module "Rollback"
|
|
return $true
|
|
}
|
|
|
|
"ScheduledTask" {
|
|
Write-Log -Level INFO -Message "Restoring scheduled task from: $BackupFile" -Module "Rollback"
|
|
|
|
try {
|
|
$taskData = Get-Content -Path $BackupFile -Raw | ConvertFrom-Json
|
|
|
|
# Import task XML if exists
|
|
if ($taskData.XmlDefinition) {
|
|
# Register-ScheduledTask requires TaskName and Xml (string)
|
|
# Force overwrite if exists
|
|
Register-ScheduledTask -Xml $taskData.XmlDefinition -TaskName $taskData.TaskName -Force | Out-Null
|
|
Write-Log -Level SUCCESS -Message "Scheduled task restored: $($taskData.TaskName)" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No XML definition found in backup for task: $($taskData.TaskName)" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to restore scheduled task" -Module "Rollback" -ErrorRecord $_
|
|
return $false
|
|
}
|
|
}
|
|
|
|
default {
|
|
Write-Log -Level ERROR -Message "Unknown backup type: $Type" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to restore from backup file: $BackupFilePath" -Module "Rollback" -ErrorRecord $_
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Invoke-RestoreRebootPrompt {
|
|
<#
|
|
.SYNOPSIS
|
|
Prompt user for system reboot after restore
|
|
|
|
.DESCRIPTION
|
|
Offers immediate or deferred reboot with countdown.
|
|
Uses validation loop for consistent behavior.
|
|
|
|
.OUTPUTS
|
|
None
|
|
#>
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host " SYSTEM REBOOT RECOMMENDED" -ForegroundColor Yellow
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# Check if Privacy module was restored with non-restorable apps
|
|
if ($script:PrivacyNonRestorableApps -and $script:PrivacyNonRestorableApps.Count -gt 0) {
|
|
Write-Host "MANUAL ACTION REQUIRED:" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host "The following apps were removed during hardening but cannot be" -ForegroundColor Gray
|
|
Write-Host "automatically restored via winget (not available in catalog):" -ForegroundColor Gray
|
|
Write-Host ""
|
|
foreach ($app in $script:PrivacyNonRestorableApps) {
|
|
Write-Host " - $app" -ForegroundColor White
|
|
}
|
|
Write-Host ""
|
|
Write-Host "Please reinstall these apps manually from the Microsoft Store" -ForegroundColor Gray
|
|
Write-Host "after the reboot if you need them." -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
}
|
|
|
|
Write-Host "RECOMMENDED: Reboot after restore" -ForegroundColor White
|
|
Write-Host ""
|
|
Write-Host "Some security settings require a reboot to be fully activated:" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " - Group Policy changes (processed but not fully active)" -ForegroundColor Gray
|
|
Write-Host " - Security Template settings (user rights, audit)" -ForegroundColor Gray
|
|
Write-Host " - Registry policies affecting boot-time services" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host "While gpupdate has processed the restored policies, a reboot" -ForegroundColor Gray
|
|
Write-Host "ensures complete activation of all security settings." -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# Prompt user with validation loop
|
|
do {
|
|
Write-Host "Reboot now? [Y/N] (default: Y): " -NoNewline -ForegroundColor White
|
|
$choice = Read-Host
|
|
if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "Y" }
|
|
$choice = $choice.Trim().ToUpper()
|
|
|
|
if ($choice -notin @('Y', 'N')) {
|
|
Write-Host ""
|
|
Write-Host "Invalid input. Please enter Y or N." -ForegroundColor Red
|
|
Write-Host ""
|
|
}
|
|
} while ($choice -notin @('Y', 'N'))
|
|
|
|
if ($choice -eq 'Y') {
|
|
Write-Host ""
|
|
Write-Host "[>] Initiating system reboot in 10 seconds..." -ForegroundColor Yellow
|
|
Write-Host " Press Ctrl+C to cancel" -ForegroundColor Gray
|
|
Write-Host ""
|
|
|
|
# Countdown from 10
|
|
for ($i = 10; $i -gt 0; $i--) {
|
|
Write-Host " Rebooting in $i seconds..." -ForegroundColor Yellow
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "[+] Rebooting system now..." -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
# Reboot
|
|
Restart-Computer -Force
|
|
}
|
|
else {
|
|
Write-Host ""
|
|
Write-Host "[!] Reboot deferred" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host "IMPORTANT: Please reboot manually at your earliest convenience." -ForegroundColor White
|
|
Write-Host "Some restored settings may not be fully active until after reboot." -ForegroundColor Gray
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
function Restore-AllBackups {
|
|
<#
|
|
.SYNOPSIS
|
|
Restore all backups from current session (full rollback)
|
|
|
|
.OUTPUTS
|
|
Boolean indicating overall success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param()
|
|
|
|
Write-Log -Level WARNING -Message "Starting full rollback of all changes" -Module "Rollback"
|
|
|
|
$allSucceeded = $true
|
|
|
|
# Restore in reverse order (LIFO)
|
|
$reversedIndex = $global:BackupIndex | Sort-Object -Property Timestamp -Descending
|
|
|
|
foreach ($backup in $reversedIndex) {
|
|
$success = Restore-FromBackup -BackupFile $backup.BackupFile -Type $backup.Type
|
|
|
|
if (-not $success) {
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
# Delete newly created registry keys (they didn't exist before)
|
|
if ($global:NewlyCreatedKeys.Count -gt 0) {
|
|
Write-Log -Level INFO -Message "Removing $($global:NewlyCreatedKeys.Count) newly created registry keys..." -Module "Rollback"
|
|
|
|
# Sort in reverse order (deepest keys first) to avoid errors
|
|
$sortedKeys = $global:NewlyCreatedKeys | Sort-Object -Property Length -Descending
|
|
|
|
foreach ($keyPath in $sortedKeys) {
|
|
try {
|
|
if (Test-Path -Path $keyPath) {
|
|
Remove-Item -Path $keyPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level INFO -Message "Deleted newly created key: $keyPath" -Module "Rollback"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to delete newly created key: $keyPath - $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($allSucceeded) {
|
|
Write-Log -Level SUCCESS -Message "Full rollback completed successfully" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Full rollback completed with some failures" -Module "Rollback"
|
|
}
|
|
|
|
# Prompt for reboot after restore
|
|
Invoke-RestoreRebootPrompt
|
|
|
|
return $allSucceeded
|
|
}
|
|
|
|
function Get-BackupSessions {
|
|
<#
|
|
.SYNOPSIS
|
|
Get list of all backup sessions
|
|
|
|
.PARAMETER BackupDirectory
|
|
Directory containing backup sessions
|
|
|
|
.OUTPUTS
|
|
Array of session objects with manifest data
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([PSCustomObject[]])]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BackupDirectory = (Join-Path $PSScriptRoot "..\Backups")
|
|
)
|
|
|
|
if (-not (Test-Path $BackupDirectory)) {
|
|
return @()
|
|
}
|
|
|
|
$sessions = @()
|
|
$sessionFolders = Get-ChildItem -Path $BackupDirectory -Directory | Where-Object { $_.Name -match '^Session_\d{8}_\d{6}$' }
|
|
|
|
foreach ($folder in $sessionFolders) {
|
|
$manifestPath = Join-Path $folder.FullName "manifest.json"
|
|
|
|
if (Test-Path $manifestPath) {
|
|
try {
|
|
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
|
|
|
$sessions += [PSCustomObject]@{
|
|
SessionId = $manifest.sessionId
|
|
Timestamp = [DateTime]::Parse($manifest.timestamp)
|
|
FrameworkVersion = $manifest.frameworkVersion
|
|
Modules = $manifest.modules
|
|
TotalItems = $manifest.totalItems
|
|
Restorable = $manifest.restorable
|
|
SessionPath = $manifest.sessionPath
|
|
FolderPath = $folder.FullName
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to read manifest for session: $($folder.Name)" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Ensure we return an array (Sort-Object can return single object unwrapped)
|
|
$sorted = @($sessions | Sort-Object -Property Timestamp -Descending)
|
|
return $sorted
|
|
}
|
|
|
|
function Get-SessionManifest {
|
|
<#
|
|
.SYNOPSIS
|
|
Get manifest for a specific session
|
|
|
|
.PARAMETER SessionPath
|
|
Path to the session folder
|
|
|
|
.OUTPUTS
|
|
Session manifest object
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([PSCustomObject])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionPath
|
|
)
|
|
|
|
$manifestPath = Join-Path $SessionPath "manifest.json"
|
|
|
|
if (-not (Test-Path $manifestPath)) {
|
|
throw "Session manifest not found: $manifestPath"
|
|
}
|
|
|
|
return Get-Content $manifestPath -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Initialize-RestoreLog {
|
|
<#
|
|
.SYNOPSIS
|
|
Initialize separate detailed log file for restore operations
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionPath
|
|
)
|
|
|
|
try {
|
|
$logsDir = Join-Path (Split-Path $PSScriptRoot -Parent) "Logs"
|
|
if (-not (Test-Path $logsDir)) {
|
|
New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
|
|
}
|
|
|
|
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$sessionId = Split-Path $SessionPath -Leaf
|
|
$restoreLogFile = "RESTORE_$($sessionId)_$timestamp.log"
|
|
$script:RestoreLogPath = Join-Path $logsDir $restoreLogFile
|
|
|
|
# Initialize restore log file
|
|
$header = @(
|
|
"================================================================"
|
|
" NoID Privacy - RESTORE LOG"
|
|
" Session: $sessionId"
|
|
" Restore Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
"================================================================"
|
|
""
|
|
)
|
|
$header | Out-File -FilePath $script:RestoreLogPath -Encoding UTF8
|
|
|
|
Write-Log -Level INFO -Message "Restore log initialized: $script:RestoreLogPath" -Module "Rollback"
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to initialize restore log: $_" -Module "Rollback"
|
|
$script:RestoreLogPath = $null
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Write-RestoreLog {
|
|
<#
|
|
.SYNOPSIS
|
|
Write to restore-specific log (in addition to main log)
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Message,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateSet('INFO', 'SUCCESS', 'WARNING', 'ERROR', 'DEBUG')]
|
|
[string]$Level = 'INFO'
|
|
)
|
|
|
|
if (-not $script:RestoreLogPath) { return }
|
|
|
|
try {
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logEntry = "[$timestamp] [$Level] $Message"
|
|
$logEntry | Out-File -FilePath $script:RestoreLogPath -Append -Encoding UTF8
|
|
}
|
|
catch {
|
|
# Silently fail to avoid breaking restore operation
|
|
$null = $null
|
|
}
|
|
}
|
|
|
|
function Restore-Session {
|
|
<#
|
|
.SYNOPSIS
|
|
Restore complete session (all modules)
|
|
|
|
.PARAMETER SessionPath
|
|
Path to the session folder
|
|
|
|
.PARAMETER ModuleNames
|
|
Optional array of specific module names to restore (restores all if not specified)
|
|
|
|
.OUTPUTS
|
|
Boolean indicating overall success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string[]]$ModuleNames
|
|
)
|
|
|
|
if (-not (Test-Path $SessionPath)) {
|
|
Write-Log -Level ERROR -Message "Session path not found: $SessionPath" -Module "Rollback"
|
|
return $false
|
|
}
|
|
|
|
# Track restore duration
|
|
$startTime = Get-Date
|
|
|
|
# CRITICAL: Initialize separate restore log (ONLY for restore operations)
|
|
Initialize-RestoreLog -SessionPath $SessionPath
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
Write-RestoreLog -Level INFO -Message "RESTORE SESSION START"
|
|
Write-RestoreLog -Level INFO -Message "Session Path: $SessionPath"
|
|
if ($ModuleNames) {
|
|
Write-RestoreLog -Level INFO -Message "Specific Modules: $($ModuleNames -join ', ')"
|
|
} else {
|
|
Write-RestoreLog -Level INFO -Message "Restoring: ALL modules"
|
|
}
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
Write-RestoreLog -Level INFO -Message " "
|
|
|
|
try {
|
|
$manifest = Get-SessionManifest -SessionPath $SessionPath
|
|
|
|
Write-Log -Level INFO -Message "Starting session restore: $($manifest.sessionId)" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Session ID: $($manifest.sessionId)"
|
|
|
|
Write-Log -Level INFO -Message "Session created: $($manifest.timestamp)" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Session Created: $($manifest.timestamp)"
|
|
|
|
Write-Log -Level INFO -Message "Total items: $($manifest.totalItems)" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Total Items Backed Up: $($manifest.totalItems)"
|
|
Write-RestoreLog -Level INFO -Message " "
|
|
|
|
$allSucceeded = $true
|
|
$modulesToRestore = if ($ModuleNames) {
|
|
$manifest.modules | Where-Object { $ModuleNames -contains $_.name }
|
|
}
|
|
else {
|
|
$manifest.modules
|
|
}
|
|
|
|
# Restore in reverse order (LIFO - last applied, first restored)
|
|
$reversedModules = $modulesToRestore | Sort-Object -Property timestamp -Descending
|
|
|
|
foreach ($moduleInfo in $reversedModules) {
|
|
Write-Log -Level INFO -Message "Restoring module: $($moduleInfo.name) ($($moduleInfo.itemsBackedUp) items)" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
Write-RestoreLog -Level INFO -Message "MODULE: $($moduleInfo.name)"
|
|
Write-RestoreLog -Level INFO -Message "Items Backed Up: $($moduleInfo.itemsBackedUp)"
|
|
Write-RestoreLog -Level INFO -Message "Backup Path: $($moduleInfo.backupPath)"
|
|
Write-RestoreLog -Level INFO -Message "Timestamp: $($moduleInfo.timestamp)"
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
|
|
$moduleBackupPath = Join-Path $SessionPath $moduleInfo.backupPath
|
|
|
|
if (-not (Test-Path $moduleBackupPath)) {
|
|
Write-Log -Level ERROR -Message "Module backup path not found: $moduleBackupPath" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "CRITICAL: Module backup path not found: $moduleBackupPath"
|
|
$allSucceeded = $false
|
|
continue
|
|
}
|
|
Write-RestoreLog -Level DEBUG -Message "Backup path verified: $moduleBackupPath"
|
|
|
|
# Pre-restore cleanup: Clear active policies BEFORE restoring backups
|
|
# This ensures hardened settings don't interfere with backup restore
|
|
|
|
if ($moduleInfo.name -eq "SecurityBaseline") {
|
|
# Detect domain-joined systems to avoid disrupting domain GPO cache
|
|
$isDomainJoined = $false
|
|
try {
|
|
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
|
$isDomainJoined = $cs.PartOfDomain
|
|
}
|
|
catch {
|
|
$isDomainJoined = $false
|
|
}
|
|
Write-Log -Level INFO -Message "Restoring SecurityBaseline from LocalGPO backup..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "[STEP 1] LocalGPO Restore"
|
|
|
|
# STEP 1: Restore full LocalGPO directory from backup
|
|
# This is the official MS method to restore ALL GPO settings at once
|
|
# More reliable than Clear + Restore-RegistryPolicies (avoids permission issues)
|
|
$localGPOBackup = Join-Path $moduleBackupPath "LocalGPO"
|
|
$localGPOPath = "C:\Windows\System32\GroupPolicy"
|
|
Write-RestoreLog -Level DEBUG -Message "LocalGPO backup path: $localGPOBackup"
|
|
Write-RestoreLog -Level DEBUG -Message "LocalGPO target path: $localGPOPath"
|
|
|
|
if ($isDomainJoined) {
|
|
Write-Log -Level WARNING -Message "Domain-joined system detected - skipping LocalGPO delete/restore to avoid interfering with domain GPO cache" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Domain-joined system - Local Group Policy folder will NOT be modified. Please restore via domain GPO if required."
|
|
}
|
|
elseif (Test-Path $localGPOBackup) {
|
|
Write-Log -Level INFO -Message "Restoring LocalGPO directory from backup..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "LocalGPO backup found - restoring full directory"
|
|
|
|
try {
|
|
# Delete current GPO directory if it exists
|
|
if (-not $isDomainJoined -and (Test-Path $localGPOPath)) {
|
|
Write-RestoreLog -Level DEBUG -Message "Removing current LocalGPO directory..."
|
|
Remove-Item -Path $localGPOPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level INFO -Message "Removed current LocalGPO directory" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Current LocalGPO removed"
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "No current LocalGPO directory to remove"
|
|
}
|
|
|
|
# Restore backup
|
|
Write-RestoreLog -Level DEBUG -Message "Copying LocalGPO backup to system..."
|
|
Copy-Item -Path $localGPOBackup -Destination $localGPOPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "LocalGPO directory restored from backup" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "LocalGPO directory restored successfully"
|
|
|
|
# Force Group Policy update to apply restored settings
|
|
Write-Log -Level INFO -Message "Applying restored Group Policy settings (gpupdate)..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "[STEP 1.1] Running gpupdate /force..."
|
|
$gpupdateProcess = Start-Process -FilePath "gpupdate.exe" -ArgumentList "/force" -Wait -NoNewWindow -PassThru
|
|
Write-RestoreLog -Level DEBUG -Message "gpupdate exit code: $($gpupdateProcess.ExitCode)"
|
|
|
|
if ($gpupdateProcess.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Group Policy settings applied successfully" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "gpupdate completed successfully"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "gpupdate returned exit code $($gpupdateProcess.ExitCode) - continuing" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "gpupdate returned non-zero exit code: $($gpupdateProcess.ExitCode)"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore LocalGPO directory: $($_.Exception.Message)" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "LocalGPO restore FAILED: $($_.Exception.Message)"
|
|
Write-RestoreLog -Level ERROR -Message "Stack trace: $($_.ScriptStackTrace)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "No LocalGPO backup found (system was clean before hardening) - clearing current GPO" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "No LocalGPO backup found - system was clean before hardening"
|
|
|
|
# System had no GPO before hardening - just clear current GPO
|
|
if (Test-Path $localGPOPath) {
|
|
try {
|
|
Write-RestoreLog -Level DEBUG -Message "Removing current LocalGPO (cleanup)..."
|
|
Remove-Item -Path $localGPOPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "Cleared LocalGPO directory (system was clean)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "LocalGPO cleared successfully"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not clear LocalGPO: $_" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "LocalGPO cleanup failed: $_"
|
|
}
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "No LocalGPO directory exists (correct state)"
|
|
}
|
|
}
|
|
|
|
# STEP 1.5: Explicitly restore registry policies from JSON backup (counter GPO tattooing)
|
|
# GPO tattooing: When GPO sets registry values and is then removed, values persist
|
|
# Solution: Explicitly restore original values from JSON backup using Restore-RegistryPolicies
|
|
Write-RestoreLog -Level INFO -Message "[STEP 2] Registry Policies Restore (counter GPO tattooing)"
|
|
$regBackupJson = Join-Path $moduleBackupPath "RegistryPolicies.json"
|
|
Write-RestoreLog -Level DEBUG -Message "Registry backup JSON: $regBackupJson"
|
|
if (Test-Path $regBackupJson) {
|
|
Write-Log -Level INFO -Message "Restoring registry policies from JSON backup (countering GPO tattooing)..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Registry backup found - restoring original values"
|
|
|
|
try {
|
|
# Load restore function if not in scope
|
|
if (-not (Get-Command "Restore-RegistryPolicies" -ErrorAction SilentlyContinue)) {
|
|
Write-RestoreLog -Level DEBUG -Message "Loading Restore-RegistryPolicies function..."
|
|
$funcPath = Join-Path $PSScriptRoot "..\Modules\SecurityBaseline\Private\Restore-RegistryPolicies.ps1"
|
|
Write-RestoreLog -Level DEBUG -Message "Function path: $funcPath"
|
|
if (Test-Path $funcPath) {
|
|
. $funcPath
|
|
Write-Log -Level DEBUG -Message "Loaded Restore-RegistryPolicies function" -Module "Rollback"
|
|
Write-RestoreLog -Level DEBUG -Message "Function loaded successfully"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Restore-RegistryPolicies.ps1 not found - skipping explicit registry restore" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "Restore-RegistryPolicies.ps1 NOT FOUND - registry restore skipped!"
|
|
}
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "Restore-RegistryPolicies function already loaded"
|
|
}
|
|
|
|
if (Get-Command "Restore-RegistryPolicies" -ErrorAction SilentlyContinue) {
|
|
Write-RestoreLog -Level DEBUG -Message "Calling Restore-RegistryPolicies..."
|
|
# Call restore function directly with combined JSON backup
|
|
$restoreResult = Restore-RegistryPolicies -BackupPath $regBackupJson
|
|
Write-RestoreLog -Level DEBUG -Message "Restore function returned - Success: $($restoreResult.Success)"
|
|
|
|
if ($restoreResult.Success) {
|
|
Write-Log -Level SUCCESS -Message "Registry policies restored: $($restoreResult.ItemsRestored) items (GPO tattooing countered)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Registry policies restored: $($restoreResult.ItemsRestored) items"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Registry restore had errors: $($restoreResult.Errors.Count) errors" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Registry restore had $($restoreResult.Errors.Count) errors:"
|
|
foreach ($err in $restoreResult.Errors) {
|
|
Write-Log -Level DEBUG -Message " - $err" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message " - $err"
|
|
}
|
|
}
|
|
|
|
# CRITICAL FIX: Terminal Services GPO Cleanup
|
|
# After restore, the Terminal Services key may exist but be empty (all values deleted).
|
|
# Verify checks expect the key to NOT exist if system was clean before apply.
|
|
# Solution: Remove the key if it's completely empty after restore.
|
|
Write-RestoreLog -Level INFO -Message "[FIX 2/3] Checking Terminal Services GPO cleanup..."
|
|
$tsKey = "HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services"
|
|
if (Test-Path $tsKey) {
|
|
Write-RestoreLog -Level DEBUG -Message "Terminal Services key exists: $tsKey"
|
|
try {
|
|
$tsProps = Get-ItemProperty -Path $tsKey -ErrorAction SilentlyContinue
|
|
$propNames = @()
|
|
if ($tsProps) {
|
|
$propNames = $tsProps.PSObject.Properties.Name | Where-Object {
|
|
$_ -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSProvider', 'PSDrive')
|
|
}
|
|
}
|
|
Write-RestoreLog -Level DEBUG -Message "Terminal Services key value count: $($propNames.Count)"
|
|
|
|
if ($propNames.Count -eq 0) {
|
|
Remove-Item -Path $tsKey -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Log -Level INFO -Message "Removed empty Terminal Services policy key (GPO cleanup - system was clean before hardening)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Terminal Services key removed (was empty - system was clean)"
|
|
}
|
|
else {
|
|
Write-Log -Level DEBUG -Message "Terminal Services key has $($propNames.Count) values - keeping key" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Terminal Services key has $($propNames.Count) values - keeping key"
|
|
Write-RestoreLog -Level DEBUG -Message "Values: $($propNames -join ', ')"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Could not check/clean Terminal Services key: $_" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Terminal Services cleanup failed: $_"
|
|
}
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "Terminal Services key does not exist (correct state)"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore registry policies from JSON: $($_.Exception.Message)" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "Registry restore exception: $($_.Exception.Message)"
|
|
Write-RestoreLog -Level ERROR -Message "Stack trace: $($_.ScriptStackTrace)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No RegistryPolicies.json backup found - GPO restore only (tattooing may occur)" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "No RegistryPolicies.json backup found - GPO tattooing may occur!"
|
|
}
|
|
|
|
# STEP 3: Restore Audit Policies from pre-hardening backup
|
|
Write-RestoreLog -Level INFO -Message "[STEP 3] Audit Policies Restore"
|
|
$auditBackupFile = Join-Path $moduleBackupPath "AuditPolicies.csv"
|
|
Write-RestoreLog -Level DEBUG -Message "Audit backup file: $auditBackupFile"
|
|
if (Test-Path $auditBackupFile) {
|
|
Write-Log -Level INFO -Message "Found audit policy backup" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Audit backup found - restoring..."
|
|
Write-Log -Level INFO -Message "Restoring audit policies from backup..." -Module "Rollback"
|
|
|
|
try {
|
|
$auditRestoreProcess = Start-Process -FilePath "auditpol.exe" `
|
|
-ArgumentList "/restore", "/file:`"$auditBackupFile`"" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru
|
|
|
|
if ($auditRestoreProcess.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Audit policies restored from pre-hardening backup" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Audit policies restored successfully"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Audit policy restore had errors (Exit: $($auditRestoreProcess.ExitCode)) - continuing" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Audit restore had errors (Exit: $($auditRestoreProcess.ExitCode))"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Audit policy restore failed: $_ - continuing" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "Audit restore exception: $_"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No pre-hardening audit policy backup found - skipping audit restore (keeping current state)" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "No audit backup found - skipping"
|
|
}
|
|
|
|
# STEP 3.5: Clear Security Template Registry Values not in secedit export
|
|
# CRITICAL FIX: secedit /export only exports values explicitly set in Security DB.
|
|
# Values on Windows-Default are NOT exported, so they won't be restored by secedit /configure.
|
|
# These 8 values are set by SecurityBaseline but may not exist in backup INF:
|
|
Write-RestoreLog -Level INFO -Message "[STEP 3.5] Clearing Security Template Registry Values (secedit gap fix)"
|
|
$secTemplateRegValues = @(
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Services\LanmanWorkstation\Parameters"; Name = "RequireSecuritySignature" },
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "allownullsessionfallback" },
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Control\Lsa"; Name = "LmCompatibilityLevel" },
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Control\Lsa"; Name = "SCENoApplyLegacyAuditPolicy" },
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Services\LanManServer\Parameters"; Name = "requiresecuritysignature" },
|
|
@{ Path = "HKLM:\System\CurrentControlSet\Control\Lsa"; Name = "RestrictRemoteSAM" },
|
|
@{ Path = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System"; Name = "FilterAdministratorToken" },
|
|
@{ Path = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System"; Name = "InactivityTimeoutSecs" }
|
|
)
|
|
|
|
$clearedSecTemplateValues = 0
|
|
foreach ($regVal in $secTemplateRegValues) {
|
|
try {
|
|
if (Test-Path $regVal.Path) {
|
|
$existingVal = Get-ItemProperty -Path $regVal.Path -Name $regVal.Name -ErrorAction SilentlyContinue
|
|
if ($existingVal) {
|
|
Remove-ItemProperty -Path $regVal.Path -Name $regVal.Name -Force -ErrorAction Stop
|
|
$clearedSecTemplateValues++
|
|
Write-RestoreLog -Level DEBUG -Message "Cleared: $($regVal.Path)\$($regVal.Name)"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-RestoreLog -Level DEBUG -Message "Could not clear $($regVal.Path)\$($regVal.Name): $_"
|
|
}
|
|
}
|
|
Write-RestoreLog -Level SUCCESS -Message "Cleared $clearedSecTemplateValues Security Template registry values (secedit gap)"
|
|
|
|
# STEP 4: Restore Security Template
|
|
Write-RestoreLog -Level INFO -Message "[STEP 4] Security Template Restore"
|
|
|
|
# Fail-Safe for Restore-SecurityTemplate (Module Scope Fix)
|
|
if (-not (Get-Command "Restore-SecurityTemplate" -ErrorAction SilentlyContinue)) {
|
|
$funcPath = Join-Path $PSScriptRoot "..\Modules\SecurityBaseline\Private\Restore-SecurityTemplate.ps1"
|
|
if (Test-Path $funcPath) { . $funcPath }
|
|
}
|
|
|
|
$rollbackTemplateFile = Join-Path $moduleBackupPath "StandaloneDelta_Rollback.inf"
|
|
if (Test-Path $rollbackTemplateFile) {
|
|
Write-Log -Level INFO -Message "Found rollback template for standalone delta" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Using StandaloneDelta_Rollback.inf"
|
|
$secTemplatResult = Restore-SecurityTemplate -BackupPath $rollbackTemplateFile
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "No rollback template found - using full security policy backup (expected)" -Module "Rollback"
|
|
Write-RestoreLog -Level DEBUG -Message "No StandaloneDelta - using SecurityTemplate.inf"
|
|
$secPolicyBackupFile = Join-Path $moduleBackupPath "SecurityTemplate.inf"
|
|
if (Test-Path $secPolicyBackupFile) {
|
|
Write-Log -Level INFO -Message "Found security template backup" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Security template backup found - restoring via secedit..."
|
|
$secTemplatResult = Restore-SecurityTemplate -BackupPath $secPolicyBackupFile
|
|
Write-RestoreLog -Level SUCCESS -Message "Security template restored"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No security policy backups found - skipping secedit restore" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "No security template backup found - skipping"
|
|
$secTemplatResult = $true
|
|
}
|
|
}
|
|
|
|
if (-not $secTemplatResult) {
|
|
Write-Log -Level WARNING -Message "Security template restore had errors - continuing" -Module "Rollback"
|
|
}
|
|
|
|
# STEP 5: Restore Xbox Task if it was disabled
|
|
$xboxTaskBackup = Join-Path $moduleBackupPath "XboxTask.json"
|
|
if (Test-Path $xboxTaskBackup) {
|
|
try {
|
|
$taskData = Get-Content $xboxTaskBackup -Raw | ConvertFrom-Json
|
|
|
|
if ($taskData.TaskExists -and $taskData.WasEnabled) {
|
|
Write-Log -Level INFO -Message "Re-enabling Xbox scheduled task (was enabled before hardening)..." -Module "Rollback"
|
|
|
|
Enable-ScheduledTask -TaskName $taskData.TaskName -TaskPath $taskData.TaskPath -ErrorAction Stop | Out-Null
|
|
Write-Log -Level SUCCESS -Message "Xbox task re-enabled: $($taskData.TaskName)" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "Xbox task was not enabled before hardening - leaving disabled" -Module "Rollback"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore Xbox task state: $_" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($moduleInfo.name -eq "ASR") {
|
|
Write-Log -Level INFO -Message "Clearing ASR configuration before restore..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "[ASR] Clearing all ASR configurations..."
|
|
|
|
# Clear MpPreference-based ASR rules
|
|
$asrClearResult = Clear-ASRRules
|
|
if (-not $asrClearResult) {
|
|
Write-Log -Level WARNING -Message "ASR rules clear had errors - continuing" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "MpPreference ASR clear had errors"
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level SUCCESS -Message "MpPreference ASR rules cleared"
|
|
}
|
|
|
|
# CRITICAL: Also clear Registry-based ASR rules (set by SecurityBaseline via GPO)
|
|
# These are in: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\ASR\Rules
|
|
$asrPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\ASR"
|
|
$asrRulesPath = "$asrPolicyPath\Rules"
|
|
try {
|
|
if (Test-Path $asrRulesPath) {
|
|
Remove-Item -Path $asrRulesPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "Cleared Registry-based ASR rules (GPO path)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Registry ASR rules cleared (GPO path)"
|
|
}
|
|
if (Test-Path $asrPolicyPath) {
|
|
# Also remove the ExploitGuard_ASR_Rules flag from the parent key
|
|
Remove-ItemProperty -Path $asrPolicyPath -Name "ExploitGuard_ASR_Rules" -ErrorAction SilentlyContinue
|
|
Write-RestoreLog -Level DEBUG -Message "Removed ExploitGuard_ASR_Rules flag"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not clear Registry ASR rules: $_" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Registry ASR clear error: $_"
|
|
}
|
|
|
|
# CRITICAL FIX: In multi-module sessions, SecurityBaseline applies 15 ASR rules BEFORE
|
|
# the ASR module runs. This means ASR_ActiveConfiguration.json captures the WRONG state
|
|
# (post-SecurityBaseline, not pre-hardening). PreFramework_Snapshot.json is created
|
|
# BEFORE any module runs and has the TRUE pre-hardening state.
|
|
#
|
|
# Priority order:
|
|
# 1. PreFramework_Snapshot.json (if exists) - TRUE pre-hardening state
|
|
# 2. ASR_ActiveConfiguration.json (fallback) - only correct for single-module ASR runs
|
|
|
|
$preFrameworkPath = Join-Path $SessionPath "PreFramework_Snapshot.json"
|
|
$usePreFramework = $false
|
|
$asrRulesToRestore = @()
|
|
|
|
if (Test-Path $preFrameworkPath) {
|
|
Write-Log -Level INFO -Message "Found PreFramework_Snapshot.json - using TRUE pre-hardening ASR state" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Using PreFramework_Snapshot.json for TRUE pre-hardening state"
|
|
try {
|
|
$preFramework = Get-Content $preFrameworkPath -Raw | ConvertFrom-Json
|
|
if ($preFramework.ASR) {
|
|
$usePreFramework = $true
|
|
# Build rules array from PreFramework snapshot
|
|
if ($preFramework.ASR.RuleIds -and $preFramework.ASR.RuleIds.Count -gt 0) {
|
|
for ($i = 0; $i -lt $preFramework.ASR.RuleIds.Count; $i++) {
|
|
if ($preFramework.ASR.RuleActions[$i] -ne 0) {
|
|
$asrRulesToRestore += @{
|
|
GUID = $preFramework.ASR.RuleIds[$i]
|
|
Action = $preFramework.ASR.RuleActions[$i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Write-Log -Level DEBUG -Message "PreFramework snapshot: $($preFramework.ASR.RuleCount) total rules, $($asrRulesToRestore.Count) active" -Module "Rollback"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to parse PreFramework_Snapshot.json: $_ - falling back to module backup" -Module "Rollback"
|
|
$usePreFramework = $false
|
|
}
|
|
}
|
|
|
|
# Fallback to module-level backup if PreFramework not available
|
|
if (-not $usePreFramework) {
|
|
$asrMpPrefBackup = Get-ChildItem -Path $moduleBackupPath -Filter "ASR_ActiveConfiguration.json" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
|
|
if ($asrMpPrefBackup) {
|
|
Write-Log -Level INFO -Message "Using ASR_ActiveConfiguration.json (single-module or legacy backup)" -Module "Rollback"
|
|
try {
|
|
$asrBackupData = Get-Content $asrMpPrefBackup.FullName -Raw | ConvertFrom-Json
|
|
if ($asrBackupData.Rules) {
|
|
$asrRulesToRestore = $asrBackupData.Rules | Where-Object { $_.Action -ne 0 }
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to parse ASR_ActiveConfiguration.json: $_" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No ASR backup found - ASR rules will remain cleared" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Apply the rules (from either source)
|
|
if ($asrRulesToRestore.Count -gt 0) {
|
|
try {
|
|
$ruleIds = $asrRulesToRestore | ForEach-Object { $_.GUID }
|
|
$ruleActions = $asrRulesToRestore | ForEach-Object { $_.Action }
|
|
|
|
Set-MpPreference -AttackSurfaceReductionRules_Ids $ruleIds `
|
|
-AttackSurfaceReductionRules_Actions $ruleActions `
|
|
-ErrorAction Stop
|
|
|
|
$sourceDesc = if ($usePreFramework) { "PreFramework snapshot (TRUE pre-hardening)" } else { "ASR_ActiveConfiguration.json" }
|
|
Write-Log -Level SUCCESS -Message "ASR rules restored via Set-MpPreference ($($asrRulesToRestore.Count) active rules from $sourceDesc)" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore ASR via Set-MpPreference: $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
# System had 0 active ASR rules before hardening (Clean State)
|
|
# Clear-ASRRules already did the job, and Registry rules were also cleared.
|
|
$sourceDesc = if ($usePreFramework) { "PreFramework snapshot" } else { "ASR backup" }
|
|
Write-Log -Level SUCCESS -Message "ASR: $sourceDesc contains 0 active rules. System restored to clean state (0/19 ASR rules)." -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "ASR restored to clean state (0 rules) from $sourceDesc"
|
|
}
|
|
}
|
|
|
|
# Restore all registry backups for this module
|
|
$regFiles = Get-ChildItem -Path $moduleBackupPath -Filter "*_Registry.reg" -ErrorAction SilentlyContinue
|
|
foreach ($regFile in $regFiles) {
|
|
# Special handling for AuditPolicy registry - just delete the value instead of importing
|
|
if ($regFile.Name -match "AuditPolicy_SCENoApplyLegacyAuditPolicy") {
|
|
try {
|
|
Write-Log -Level INFO -Message "Removing SCENoApplyLegacyAuditPolicy registry value..." -Module "Rollback"
|
|
Remove-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Lsa" -Name "SCENoApplyLegacyAuditPolicy" -ErrorAction SilentlyContinue
|
|
Write-Log -Level SUCCESS -Message "SCENoApplyLegacyAuditPolicy removed" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not remove SCENoApplyLegacyAuditPolicy (may not exist)" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
$success = Restore-FromBackup -BackupFile $regFile.FullName -Type "Registry"
|
|
if (-not $success) {
|
|
# Check if we have a JSON fallback (Smart Warning Suppression)
|
|
$isProtectedKey = $false
|
|
if ($moduleInfo.name -eq "AntiAI" -and $regFile.Name -match "Explorer_Advanced_Device_Registry") { $isProtectedKey = $true }
|
|
if ($moduleInfo.name -eq "AdvancedSecurity" -and ($regFile.Name -match "RDP_Settings" -or $regFile.Name -match "WPAD_")) { $isProtectedKey = $true }
|
|
|
|
if ($isProtectedKey) {
|
|
Write-Log -Level INFO -Message "Standard registry import skipped for protected key (will use Smart JSON-Fallback)." -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Registry restore failed for: $($regFile.Name) - continuing..." -Module "Rollback"
|
|
}
|
|
# Don't fail entire restore for registry errors - continue with other restores
|
|
}
|
|
}
|
|
}
|
|
|
|
# Special handling for protected registry keys (RDP, WPAD) that fail with reg.exe import
|
|
# These keys require PowerShell-based restore from JSON backups
|
|
if ($moduleInfo.name -eq "AntiAI") {
|
|
# Explorer Advanced Settings - use JSON backup if .reg import failed
|
|
$expJsonBackup = Get-ChildItem -Path $moduleBackupPath -Filter "Explorer_Advanced_Device_JSON.json" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($expJsonBackup) {
|
|
Write-Log -Level INFO -Message "Restoring Explorer Advanced settings via PowerShell (protected key)..." -Module "Rollback"
|
|
try {
|
|
$expData = Get-Content $expJsonBackup.FullName -Raw | ConvertFrom-Json
|
|
if ($null -ne $expData.ShowCopilotButton) {
|
|
$expPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
|
|
if (Test-Path $expPath) {
|
|
Set-ItemProperty -Path $expPath -Name "ShowCopilotButton" -Value $expData.ShowCopilotButton -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "Explorer Advanced settings restored via PowerShell" -Module "Rollback"
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Log -Level WARNING -Message "PowerShell-based Explorer restore failed: $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Apply AntiAI pre-state snapshot (32 policies) if available
|
|
$antiAIPreStatePath = Join-Path $moduleBackupPath "AntiAI_PreState.json"
|
|
if (Test-Path $antiAIPreStatePath) {
|
|
Write-Log -Level INFO -Message "Restoring AntiAI pre-state snapshot (32 policies)..." -Module "Rollback"
|
|
try {
|
|
$preEntries = Get-Content $antiAIPreStatePath -Raw | ConvertFrom-Json
|
|
|
|
foreach ($entry in $preEntries) {
|
|
if (-not $entry.Path -or -not $entry.Name) { continue }
|
|
|
|
if ($entry.Exists) {
|
|
# Value existed before hardening - restore original value/type
|
|
$keyPath = $entry.Path
|
|
try {
|
|
if (-not (Test-Path $keyPath)) {
|
|
New-Item -Path $keyPath -Force | Out-Null
|
|
}
|
|
|
|
$regType = switch ($entry.Type) {
|
|
"DWord" { "DWord" }
|
|
"String" { "String" }
|
|
"MultiString" { "MultiString" }
|
|
default { "String" }
|
|
}
|
|
|
|
$existing = Get-ItemProperty -Path $keyPath -Name $entry.Name -ErrorAction SilentlyContinue
|
|
if ($null -ne $existing) {
|
|
Set-ItemProperty -Path $keyPath -Name $entry.Name -Value $entry.Value -Force -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
New-ItemProperty -Path $keyPath -Name $entry.Name -Value $entry.Value -PropertyType $regType -Force -ErrorAction SilentlyContinue | Out-Null
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore AntiAI value $($entry.Path)\$($entry.Name): $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
# Value did not exist before hardening - was created during hardening, so remove it
|
|
try {
|
|
if (Test-Path $entry.Path) {
|
|
Remove-ItemProperty -Path $entry.Path -Name $entry.Name -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "AntiAI pre-state cleanup: could not remove $($entry.Path)\$($entry.Name) - $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "AntiAI pre-state snapshot applied successfully" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to apply AntiAI pre-state snapshot: $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($moduleInfo.name -eq "AdvancedSecurity") {
|
|
# RDP Settings - use JSON backup if .reg import failed
|
|
$rdpJsonBackup = Get-ChildItem -Path $moduleBackupPath -Filter "RDP_Hardening.json" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($rdpJsonBackup) {
|
|
Write-Log -Level INFO -Message "Restoring RDP settings via PowerShell (protected key)..." -Module "Rollback"
|
|
try {
|
|
$rdpData = Get-Content $rdpJsonBackup.FullName -Raw | ConvertFrom-Json
|
|
|
|
$policyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services"
|
|
$systemPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server"
|
|
|
|
# Restore Policy settings (if backed up)
|
|
if ($null -ne $rdpData.Policy_UserAuthentication) {
|
|
if (Test-Path $policyPath) {
|
|
Set-ItemProperty -Path $policyPath -Name "UserAuthentication" -Value $rdpData.Policy_UserAuthentication -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
if ($null -ne $rdpData.Policy_SecurityLayer) {
|
|
if (Test-Path $policyPath) {
|
|
Set-ItemProperty -Path $policyPath -Name "SecurityLayer" -Value $rdpData.Policy_SecurityLayer -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
|
|
# Restore System settings (if backed up)
|
|
if ($null -ne $rdpData.System_fDenyTSConnections) {
|
|
if (Test-Path $systemPath) {
|
|
Set-ItemProperty -Path $systemPath -Name "fDenyTSConnections" -Value $rdpData.System_fDenyTSConnections -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "RDP settings restored via PowerShell" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "PowerShell-based RDP restore failed: $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "RDP_Hardening.json backup not found (backup created before JSON feature was added)" -Module "Rollback"
|
|
Write-Log -Level INFO -Message "RDP settings cannot be fully restored from this backup - create new backup for complete restore" -Module "Rollback"
|
|
}
|
|
|
|
# WPAD Settings - use JSON backup if .reg import failed
|
|
$wpadJsonBackup = Get-ChildItem -Path $moduleBackupPath -Filter "WPAD.json" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($wpadJsonBackup) {
|
|
Write-Log -Level INFO -Message "Restoring WPAD settings via PowerShell (protected key)..." -Module "Rollback"
|
|
try {
|
|
$wpadData = Get-Content $wpadJsonBackup.FullName -Raw | ConvertFrom-Json
|
|
|
|
# WPAD JSON format: { "FullPath\\ValueName": value }
|
|
foreach ($property in $wpadData.PSObject.Properties) {
|
|
$fullPath = $property.Name
|
|
$lastBackslash = $fullPath.LastIndexOf('\')
|
|
|
|
if ($lastBackslash -gt 0) {
|
|
$keyPath = $fullPath.Substring(0, $lastBackslash)
|
|
$valueName = $fullPath.Substring($lastBackslash + 1)
|
|
|
|
if ($null -ne $property.Value -and (Test-Path $keyPath)) {
|
|
Set-ItemProperty -Path $keyPath -Name $valueName -Value $property.Value -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "WPAD settings restored via PowerShell" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "PowerShell-based WPAD restore failed: $($_.Exception.Message)" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "WPAD.json backup not found (backup created before JSON feature was added)" -Module "Rollback"
|
|
Write-Log -Level INFO -Message "WPAD settings cannot be fully restored from this backup - create new backup for complete restore" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Handle Empty Markers: Delete registry keys that didn't exist before hardening
|
|
$emptyMarkers = Get-ChildItem -Path $moduleBackupPath -Filter "*_EMPTY.json" -ErrorAction SilentlyContinue
|
|
foreach ($marker in $emptyMarkers) {
|
|
try {
|
|
$markerData = Get-Content $marker.FullName -Raw | ConvertFrom-Json
|
|
|
|
if ($markerData.State -eq "NotExisted" -and $markerData.KeyPath) {
|
|
Write-Log -Level INFO -Message "Processing empty marker: Registry key '$($markerData.KeyPath)' did not exist before hardening - deleting..." -Module "Rollback"
|
|
|
|
if (Test-Path $markerData.KeyPath) {
|
|
Remove-Item -Path $markerData.KeyPath -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level SUCCESS -Message "Deleted registry key (did not exist before hardening): $($markerData.KeyPath)" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "Registry key already doesn't exist: $($markerData.KeyPath)" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to process empty marker $($marker.Name): $_" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Restore all service backups for this module
|
|
$serviceFiles = Get-ChildItem -Path $moduleBackupPath -Filter "*_Service.json" -ErrorAction SilentlyContinue
|
|
foreach ($serviceFile in $serviceFiles) {
|
|
$success = Restore-FromBackup -BackupFile $serviceFile.FullName -Type "Service"
|
|
if (-not $success) {
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
# Restore all task backups for this module
|
|
$taskFiles = Get-ChildItem -Path $moduleBackupPath -Filter "*_Task.xml" -ErrorAction SilentlyContinue
|
|
foreach ($taskFile in $taskFiles) {
|
|
$success = Restore-FromBackup -BackupFile $taskFile.FullName -Type "ScheduledTask"
|
|
if (-not $success) {
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
# Special handling for DNS: Restore DNS settings from backup
|
|
if ($moduleInfo.name -eq "DNS") {
|
|
Write-Log -Level INFO -Message "Restoring DNS settings from backup..." -Module "Rollback"
|
|
|
|
# Find DNS backup file
|
|
$dnsBackupFile = Get-ChildItem -Path $moduleBackupPath -Filter "*.json" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
|
|
if ($dnsBackupFile) {
|
|
Write-Log -Level INFO -Message "Found DNS backup: $($dnsBackupFile.Name)" -Module "Rollback"
|
|
|
|
# Load DNS module for restore
|
|
$dnsModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Modules\DNS\DNS.psd1"
|
|
if (Test-Path $dnsModulePath) {
|
|
try {
|
|
Import-Module $dnsModulePath -Force -ErrorAction Stop
|
|
|
|
# Call DNS module's restore function
|
|
$restoreResult = Restore-DNSSettings -BackupFilePath $dnsBackupFile.FullName
|
|
|
|
if ($restoreResult) {
|
|
Write-Log -Level SUCCESS -Message "DNS settings restored successfully" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "DNS restore had issues - check logs" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
|
|
Remove-Module DNS -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore DNS settings: $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "DNS module not found - cannot restore DNS settings" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No DNS backup file found in: $moduleBackupPath" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Special handling for EdgeHardening: Restore Edge policy pre-state snapshot
|
|
if ($moduleInfo.name -eq "EdgeHardening") {
|
|
$edgePreStatePath = Join-Path $moduleBackupPath "EdgeHardening_PreState.json"
|
|
if (Test-Path $edgePreStatePath) {
|
|
Write-Log -Level INFO -Message "Restoring Edge policy pre-state snapshot..." -Module "Rollback"
|
|
try {
|
|
$preEntries = Get-Content $edgePreStatePath -Raw | ConvertFrom-Json
|
|
|
|
$edgeRoot = "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge"
|
|
if (Test-Path $edgeRoot) {
|
|
$keysToProcess = @()
|
|
$keysToProcess += $edgeRoot
|
|
$childKeys = Get-ChildItem -Path $edgeRoot -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer }
|
|
foreach ($child in $childKeys) {
|
|
$keysToProcess += $child.PSPath
|
|
}
|
|
|
|
foreach ($keyPath in $keysToProcess) {
|
|
try {
|
|
$currentProps = Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue
|
|
if ($currentProps) {
|
|
$propNames = $currentProps.PSObject.Properties.Name | Where-Object { $_ -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSProvider', 'PSDrive') }
|
|
foreach ($prop in $propNames) {
|
|
Remove-ItemProperty -Path $keyPath -Name $prop -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Could not clear $keyPath : $_" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
$restoredCount = 0
|
|
foreach ($entry in $preEntries) {
|
|
if (-not $entry.Path -or -not $entry.Name) { continue }
|
|
|
|
try {
|
|
if (-not (Test-Path $entry.Path)) {
|
|
New-Item -Path $entry.Path -Force -ErrorAction Stop | Out-Null
|
|
}
|
|
|
|
$regType = switch ($entry.Type) {
|
|
"DWord" { "DWord" }
|
|
"String" { "String" }
|
|
"MultiString" { "MultiString" }
|
|
"ExpandString" { "ExpandString" }
|
|
"Binary" { "Binary" }
|
|
"QWord" { "QWord" }
|
|
default { "String" }
|
|
}
|
|
|
|
New-ItemProperty -Path $entry.Path -Name $entry.Name -Value $entry.Value -PropertyType $regType -Force -ErrorAction Stop | Out-Null
|
|
$restoredCount++
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Failed to restore Edge policy value $($entry.Path)\\$($entry.Name): $_" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "Edge policy pre-state restored ($restoredCount values)" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore EdgeHardening pre-state snapshot: $_" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "No EdgeHardening pre-state snapshot found - using .reg restore + empty markers only" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Special handling for Privacy: Restore registry snapshot + removed apps
|
|
if ($moduleInfo.name -eq "Privacy") {
|
|
# STEP 1: Restore Privacy registry pre-state snapshot (counters GPO tattooing)
|
|
$privacyPreStatePath = Join-Path $moduleBackupPath "Privacy_PreState.json"
|
|
if (Test-Path $privacyPreStatePath) {
|
|
Write-Log -Level INFO -Message "Restoring Privacy registry pre-state snapshot..." -Module "Rollback"
|
|
try {
|
|
$preEntries = Get-Content $privacyPreStatePath -Raw | ConvertFrom-Json
|
|
|
|
# Build list of all keys to clear first (must match Backup-PrivacySettings list)
|
|
# CRITICAL: Include ALL keys that Privacy module modifies, including HKCU user settings!
|
|
$keysToProcess = @(
|
|
# HKLM Policy keys
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\AdvertisingInfo",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\InputPersonalization",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\System",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\SettingSync",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\LocationAndSensors",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\OneDrive",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Dsh",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\FindMyDevice",
|
|
"HKLM:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\appDiagnostics",
|
|
# HKCU Policy keys
|
|
"HKCU:\Software\Policies\Microsoft\Windows\Explorer",
|
|
# HKCU User settings (FIX: these were missing, causing restore incomplete!)
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Search",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\SearchSettings",
|
|
"HKCU:\Control Panel\International\User Profile",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\SystemSettings\AccountNotifications",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\UserProfileEngagement",
|
|
"HKCU:\SOFTWARE\Microsoft\Personalization\Settings",
|
|
# NEW: Input Personalization Settings (v2.2.0 - FIX missing HKCU restore)
|
|
"HKCU:\SOFTWARE\Microsoft\InputPersonalization",
|
|
"HKCU:\SOFTWARE\Microsoft\InputPersonalization\TrainedDataStore",
|
|
"HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\appDiagnostics"
|
|
)
|
|
|
|
# Clear all current values in Privacy keys (prepare clean slate)
|
|
foreach ($keyPath in $keysToProcess) {
|
|
if (Test-Path $keyPath) {
|
|
try {
|
|
$currentProps = Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue
|
|
if ($currentProps) {
|
|
$propNames = $currentProps.PSObject.Properties.Name | Where-Object { $_ -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSProvider') }
|
|
foreach ($prop in $propNames) {
|
|
Remove-ItemProperty -Path $keyPath -Name $prop -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Log -Level DEBUG -Message "Could not clear $keyPath : $_" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Restore original values from snapshot
|
|
$restoredCount = 0
|
|
foreach ($entry in $preEntries) {
|
|
if (-not $entry.Path -or -not $entry.Name) { continue }
|
|
|
|
try {
|
|
if (-not (Test-Path $entry.Path)) {
|
|
New-Item -Path $entry.Path -Force -ErrorAction Stop | Out-Null
|
|
}
|
|
|
|
$regType = switch ($entry.Type) {
|
|
"DWord" { "DWord" }
|
|
"String" { "String" }
|
|
"MultiString" { "MultiString" }
|
|
"ExpandString" { "ExpandString" }
|
|
"Binary" { "Binary" }
|
|
default { "String" }
|
|
}
|
|
|
|
New-ItemProperty -Path $entry.Path -Name $entry.Name -Value $entry.Value -PropertyType $regType -Force -ErrorAction Stop | Out-Null
|
|
$restoredCount++
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Failed to restore Privacy value $($entry.Path)\$($entry.Name): $_" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "Privacy registry pre-state restored ($restoredCount values)" -Module "Rollback"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore Privacy pre-state snapshot: $_" -Module "Rollback"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No Privacy pre-state snapshot found - using .reg restore only (tattooing may occur)" -Module "Rollback"
|
|
}
|
|
|
|
# STEP 2: Restore removed apps via winget (if metadata exists)
|
|
Write-Log -Level INFO -Message "Restoring removed apps for Privacy module (winget) if applicable..." -Module "Rollback"
|
|
|
|
$privacyModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Modules\Privacy\Privacy.psd1"
|
|
if (Test-Path $privacyModulePath) {
|
|
try {
|
|
Import-Module $privacyModulePath -Force -ErrorAction Stop
|
|
|
|
if (Get-Command Restore-Bloatware -ErrorAction SilentlyContinue) {
|
|
$restoreAppsResult = Restore-Bloatware -BackupPath $moduleBackupPath
|
|
|
|
# Restore-Bloatware now returns PSCustomObject with Success and NonRestorableApps properties
|
|
if ($restoreAppsResult.Success) {
|
|
Write-Log -Level SUCCESS -Message "Privacy apps restore (winget) completed" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Privacy apps restore (winget) reported issues - check logs" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
|
|
# Track non-restorable apps for user notification before reboot
|
|
if ($restoreAppsResult.NonRestorableApps -and $restoreAppsResult.NonRestorableApps.Count -gt 0) {
|
|
$script:PrivacyNonRestorableApps = $restoreAppsResult.NonRestorableApps
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Restore-Bloatware function not found in Privacy module - skipping app restore" -Module "Rollback"
|
|
}
|
|
|
|
Remove-Module Privacy -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore Privacy apps via winget: $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Privacy module not found - cannot restore removed apps" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Special handling for SecurityBaseline: Restore LocalGPO after clearing
|
|
if ($moduleInfo.name -eq "SecurityBaseline") {
|
|
$gpoBackupPath = Join-Path $moduleBackupPath "LocalGPO"
|
|
if (Test-Path $gpoBackupPath) {
|
|
Write-Log -Level INFO -Message "Restoring Local Group Policy from: $gpoBackupPath" -Module "Rollback"
|
|
|
|
try {
|
|
$gpoTargetPath = "C:\Windows\System32\GroupPolicy"
|
|
|
|
# Check if backup directory has content (not empty)
|
|
$backupContent = Get-ChildItem -Path $gpoBackupPath -Recurse -ErrorAction SilentlyContinue
|
|
|
|
if ($backupContent -and $backupContent.Count -gt 0) {
|
|
# Copy all contents from LocalGPO backup to GroupPolicy directory
|
|
Copy-Item -Path "$gpoBackupPath\*" -Destination $gpoTargetPath -Recurse -Force -ErrorAction Stop
|
|
|
|
Write-Log -Level SUCCESS -Message "Local Group Policy restored successfully from backup" -Module "Rollback"
|
|
}
|
|
else {
|
|
# Empty backup = system had no LocalGPO before hardening
|
|
Write-Log -Level INFO -Message "LocalGPO backup is empty (system was clean before hardening) - no restore needed" -Module "Rollback"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Exception restoring Local Group Policy: $($_.Exception.Message)" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No LocalGPO backup found for SecurityBaseline - policies remain cleared" -Module "Rollback"
|
|
}
|
|
}
|
|
|
|
# Special handling for AdvancedSecurity: Restore custom settings
|
|
if ($moduleInfo.name -eq "AdvancedSecurity") {
|
|
Write-Log -Level INFO -Message "Restoring Advanced Security settings..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "[ADVANCEDSECURITY] Starting restore..."
|
|
|
|
# STEP 1: Restore AdvancedSecurity registry pre-state snapshot (counters GPO tattooing)
|
|
$advSecPreStatePath = Join-Path $moduleBackupPath "AdvancedSecurity_PreState.json"
|
|
Write-RestoreLog -Level DEBUG -Message "PreState snapshot path: $advSecPreStatePath"
|
|
if (Test-Path $advSecPreStatePath) {
|
|
Write-Log -Level INFO -Message "Restoring AdvancedSecurity registry pre-state snapshot..." -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "[STEP 1] AdvancedSecurity Registry PreState Restore"
|
|
try {
|
|
Write-RestoreLog -Level DEBUG -Message "Loading PreState JSON..."
|
|
$preEntries = Get-Content $advSecPreStatePath -Raw | ConvertFrom-Json
|
|
Write-RestoreLog -Level DEBUG -Message "PreState entries loaded: $($preEntries.Count) values"
|
|
|
|
# Build list of all keys to clear first
|
|
$keysToProcess = @(
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client",
|
|
"HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters", # mDNS / Discovery Protocols
|
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp", # Official MS DisableWpad key
|
|
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Wpad", # Legacy WpadOverride
|
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings", # AutoDetect
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate",
|
|
"HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers",
|
|
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Connect", # Wireless Display / Miracast
|
|
"HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\PublicProfile", # Firewall Shields Up
|
|
"HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" # IPv6 disable (mitm6 mitigation)
|
|
)
|
|
|
|
# Clear all current values in AdvancedSecurity keys (prepare clean slate)
|
|
Write-RestoreLog -Level DEBUG -Message "Clearing current AdvancedSecurity keys (preparing clean slate)..."
|
|
Write-RestoreLog -Level DEBUG -Message "Keys to process: $($keysToProcess.Count)"
|
|
$clearedCount = 0
|
|
foreach ($keyPath in $keysToProcess) {
|
|
if (Test-Path $keyPath) {
|
|
try {
|
|
$currentProps = Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue
|
|
if ($currentProps) {
|
|
$propNames = $currentProps.PSObject.Properties.Name | Where-Object {
|
|
$_ -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSProvider', 'PSDrive')
|
|
}
|
|
foreach ($prop in $propNames) {
|
|
# Skip system-critical RDP values that should never be deleted
|
|
if ($keyPath -like "*Terminal Server*" -and $prop -in @("fSingleSessionPerUser", "TSEnabled", "TSUserEnabled")) {
|
|
continue
|
|
}
|
|
Remove-ItemProperty -Path $keyPath -Name $prop -ErrorAction SilentlyContinue
|
|
$clearedCount++
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Could not clear $keyPath : $_" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Could not clear $keyPath : $_"
|
|
}
|
|
}
|
|
}
|
|
Write-RestoreLog -Level DEBUG -Message "Cleared $clearedCount values from AdvancedSecurity keys"
|
|
|
|
# Restore original values from snapshot
|
|
Write-RestoreLog -Level DEBUG -Message "Restoring original values from PreState snapshot..."
|
|
$restoredCount = 0
|
|
$failedCount = 0
|
|
foreach ($entry in $preEntries) {
|
|
if (-not $entry.Path -or -not $entry.Name) { continue }
|
|
|
|
try {
|
|
if (-not (Test-Path $entry.Path)) {
|
|
New-Item -Path $entry.Path -Force -ErrorAction Stop | Out-Null
|
|
}
|
|
|
|
$regType = switch ($entry.Type) {
|
|
"DWord" { "DWord" }
|
|
"String" { "String" }
|
|
"MultiString" { "MultiString" }
|
|
"ExpandString" { "ExpandString" }
|
|
"Binary" { "Binary" }
|
|
"QWord" { "QWord" }
|
|
default { "String" }
|
|
}
|
|
|
|
New-ItemProperty -Path $entry.Path -Name $entry.Name -Value $entry.Value -PropertyType $regType -Force -ErrorAction Stop | Out-Null
|
|
$restoredCount++
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Failed to restore AdvancedSecurity value $($entry.Path)\$($entry.Name): $_" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "Failed to restore: $($entry.Path)\$($entry.Name) - $_"
|
|
$failedCount++
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "AdvancedSecurity registry pre-state restored ($restoredCount values)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "PreState restored: $restoredCount values restored, $failedCount failed"
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Failed to restore AdvancedSecurity pre-state snapshot: $_" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "PreState restore FAILED: $($_.Exception.Message)"
|
|
Write-RestoreLog -Level ERROR -Message "Stack trace: $($_.ScriptStackTrace)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "No AdvancedSecurity pre-state snapshot found - using .reg restore only (tattooing may occur)" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "No PreState snapshot found - using .reg restore only (tattooing may occur)"
|
|
}
|
|
|
|
# STEP 2: Find all AdvancedSecurity custom backup files (RiskyPorts, PowerShellV2, AdminShares)
|
|
$advSecBackups = Get-ChildItem -Path $moduleBackupPath -Filter "*_*.json" -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch "_Service.json" -and $_.Name -ne "AdvancedSecurity_PreState.json" }
|
|
|
|
if ($advSecBackups) {
|
|
# Load AdvancedSecurity module for restore
|
|
$advSecModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Modules\AdvancedSecurity\AdvancedSecurity.psd1"
|
|
|
|
if (Test-Path $advSecModulePath) {
|
|
try {
|
|
Import-Module $advSecModulePath -Force -ErrorAction Stop
|
|
|
|
foreach ($backupFile in $advSecBackups) {
|
|
Write-Log -Level INFO -Message "Restoring Advanced Security backup: $($backupFile.Name)" -Module "Rollback"
|
|
|
|
# Call AdvancedSecurity module's restore function
|
|
$restoreResult = Restore-AdvancedSecuritySettings -BackupFilePath $backupFile.FullName
|
|
|
|
if ($restoreResult) {
|
|
Write-Log -Level SUCCESS -Message "Restored: $($backupFile.Name)" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Failed to restore: $($backupFile.Name)" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
Remove-Module AdvancedSecurity -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore Advanced Security settings: $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "AdvancedSecurity module not found - cannot restore settings" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
# CRITICAL FIX: Restore Firewall_Rules and SMB_Shares from session root
|
|
# Bug: These backups are stored in separate folders (Firewall_Rules/, SMB_Shares/)
|
|
# in the session root, not under AdvancedSecurity/, so they were never restored.
|
|
# This caused Finger Protocol rule and Admin Shares to remain active after restore.
|
|
Write-RestoreLog -Level INFO -Message "[FIX 3a/3] Restoring Firewall_Rules and SMB_Shares from session root..."
|
|
$firewallBackupDir = Join-Path $SessionPath "Firewall_Rules"
|
|
$smbBackupDir = Join-Path $SessionPath "SMB_Shares"
|
|
Write-RestoreLog -Level DEBUG -Message "Firewall backup dir: $firewallBackupDir"
|
|
Write-RestoreLog -Level DEBUG -Message "SMB backup dir: $smbBackupDir"
|
|
|
|
foreach ($backupDir in @($firewallBackupDir, $smbBackupDir)) {
|
|
if (Test-Path $backupDir) {
|
|
Write-RestoreLog -Level DEBUG -Message "Processing backup directory: $backupDir"
|
|
$backupFiles = Get-ChildItem -Path $backupDir -Filter "*.json" -ErrorAction SilentlyContinue
|
|
Write-RestoreLog -Level DEBUG -Message "Found $($backupFiles.Count) backup file(s)"
|
|
|
|
if ($backupFiles) {
|
|
$advSecModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Modules\AdvancedSecurity\AdvancedSecurity.psd1"
|
|
|
|
if (Test-Path $advSecModulePath) {
|
|
try {
|
|
Import-Module $advSecModulePath -Force -ErrorAction Stop
|
|
|
|
foreach ($backupFile in $backupFiles) {
|
|
Write-Log -Level INFO -Message "Restoring $(Split-Path $backupDir -Leaf) backup: $($backupFile.Name)" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "Restoring: $(Split-Path $backupDir -Leaf)\$($backupFile.Name)"
|
|
|
|
$restoreResult = Restore-AdvancedSecuritySettings -BackupFilePath $backupFile.FullName
|
|
|
|
if ($restoreResult) {
|
|
Write-Log -Level SUCCESS -Message "Restored: $($backupFile.Name)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Successfully restored: $($backupFile.Name)"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Failed to restore: $($backupFile.Name)" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "FAILED to restore: $($backupFile.Name)"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
|
|
Remove-Module AdvancedSecurity -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to restore $(Split-Path $backupDir -Leaf): $_" -Module "Rollback"
|
|
$allSucceeded = $false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# CRITICAL FIX: Clean up SRP subkeys if system was clean before hardening
|
|
# Bug: PreState-Restore only clears values in root key, not the \0\Paths subkeys
|
|
# where actual SRP rules live. This caused "Block LNK" rules to remain after restore.
|
|
Write-RestoreLog -Level INFO -Message "[FIX 3b/3] Checking SRP subkey cleanup..."
|
|
$srpRootKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers"
|
|
if (Test-Path $srpRootKey) {
|
|
Write-RestoreLog -Level DEBUG -Message "SRP root key exists: $srpRootKey"
|
|
try {
|
|
# Check if PreState had ANY SRP-related entries
|
|
$hadSRPInPreState = $false
|
|
if (Test-Path $advSecPreStatePath) {
|
|
Write-RestoreLog -Level DEBUG -Message "Reading PreState from: $advSecPreStatePath"
|
|
$preEntries = Get-Content $advSecPreStatePath -Raw | ConvertFrom-Json
|
|
$srpEntries = $preEntries | Where-Object { $_.Path -like "*Safer\CodeIdentifiers*" }
|
|
$hadSRPInPreState = $srpEntries.Count -gt 0
|
|
Write-RestoreLog -Level DEBUG -Message "SRP entries in PreState: $($srpEntries.Count)"
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level WARNING -Message "PreState file not found: $advSecPreStatePath"
|
|
}
|
|
|
|
if (-not $hadSRPInPreState) {
|
|
Write-RestoreLog -Level INFO -Message "System had NO SRP rules before hardening - removing SRP subkeys"
|
|
# System had NO SRP rules before hardening - clean up all SRP subkeys
|
|
$srpPathsKey = "$srpRootKey\0\Paths"
|
|
if (Test-Path $srpPathsKey) {
|
|
Write-RestoreLog -Level DEBUG -Message "Removing SRP Paths subkey: $srpPathsKey"
|
|
Remove-Item -Path $srpPathsKey -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level INFO -Message "Removed SRP Paths subkeys (system had no SRP rules before hardening)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "SRP Paths subkeys removed successfully"
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "SRP Paths subkey does not exist (correct state)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log -Level DEBUG -Message "System had SRP rules in PreState - keeping SRP structure" -Module "Rollback"
|
|
Write-RestoreLog -Level INFO -Message "System had $($srpEntries.Count) SRP rules in PreState - keeping SRP structure"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level DEBUG -Message "Could not clean SRP subkeys: $_" -Module "Rollback"
|
|
Write-RestoreLog -Level ERROR -Message "SRP cleanup failed: $_"
|
|
}
|
|
}
|
|
else {
|
|
Write-RestoreLog -Level DEBUG -Message "SRP root key does not exist (correct state)"
|
|
}
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "Completed restore for module: $($moduleInfo.name)" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "Module $($moduleInfo.name) restore completed"
|
|
Write-RestoreLog -Level INFO -Message " "
|
|
}
|
|
|
|
if ($allSucceeded) {
|
|
Write-Log -Level SUCCESS -Message "Session restore completed successfully" -Module "Rollback"
|
|
Write-RestoreLog -Level SUCCESS -Message "========================================"
|
|
Write-RestoreLog -Level SUCCESS -Message "RESTORE COMPLETED SUCCESSFULLY"
|
|
Write-RestoreLog -Level SUCCESS -Message "All modules restored without errors"
|
|
Write-RestoreLog -Level SUCCESS -Message "========================================"
|
|
}
|
|
else {
|
|
Write-Log -Level WARNING -Message "Session restore completed with some failures" -Module "Rollback"
|
|
Write-RestoreLog -Level WARNING -Message "========================================"
|
|
Write-RestoreLog -Level WARNING -Message "RESTORE COMPLETED WITH FAILURES"
|
|
Write-RestoreLog -Level WARNING -Message "Check log above for error details"
|
|
Write-RestoreLog -Level WARNING -Message "========================================"
|
|
}
|
|
|
|
# NOTE: Pre-Framework Snapshot processing for ASR has been moved to the module-level
|
|
# restore section (see "if ($moduleInfo.name -eq "ASR")" block above).
|
|
#
|
|
# The module-level ASR restore now correctly prioritizes:
|
|
# 1. PreFramework_Snapshot.json - TRUE pre-hardening state (before SecurityBaseline runs)
|
|
# 2. ASR_ActiveConfiguration.json - fallback for single-module ASR runs
|
|
#
|
|
# This section is reserved for future non-ASR shared resources if needed.
|
|
# Currently, PreFramework_Snapshot only contains ASR data, so no action needed here.
|
|
|
|
Write-Host ""
|
|
Write-Host ""
|
|
Write-Host "============================================================================" -ForegroundColor Cyan
|
|
Write-Host "============================================================================" -ForegroundColor Cyan
|
|
if ($allSucceeded) {
|
|
Write-Host ""
|
|
Write-Host " RESTORE COMPLETED SUCCESSFULLY " -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host " All security settings have been reverted to backup state" -ForegroundColor White
|
|
Write-Host " Modules restored: $($reversedModules.Count) | Total items: $($manifest.totalItems)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
} else {
|
|
Write-Host ""
|
|
Write-Host " RESTORE COMPLETED WITH ISSUES " -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host " Some items could not be restored - check logs for details" -ForegroundColor Gray
|
|
Write-Host " Modules processed: $($reversedModules.Count) | Total items: $($manifest.totalItems)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
}
|
|
Write-Host "============================================================================" -ForegroundColor Cyan
|
|
Write-Host "============================================================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host ""
|
|
|
|
# Prompt for reboot after restore
|
|
Invoke-RestoreRebootPrompt
|
|
|
|
# Final restore log entry
|
|
$endTime = Get-Date
|
|
$duration = $endTime - $startTime
|
|
Write-RestoreLog -Level INFO -Message " "
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
Write-RestoreLog -Level INFO -Message "RESTORE SESSION END"
|
|
Write-RestoreLog -Level INFO -Message "Duration: $($duration.ToString('mm\:ss'))"
|
|
Write-RestoreLog -Level INFO -Message "Final Status: $(if ($allSucceeded) {'SUCCESS'} else {'PARTIAL FAILURE'})"
|
|
Write-RestoreLog -Level INFO -Message "Restore Log: $script:RestoreLogPath"
|
|
Write-RestoreLog -Level INFO -Message "========================================"
|
|
|
|
return $allSucceeded
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to restore hardening session: $SessionName" -Module "Rollback" -ErrorRecord $_
|
|
Write-RestoreLog -Level ERROR -Message "CRITICAL FAILURE: $_"
|
|
Write-RestoreLog -Level ERROR -Message "Restore aborted with exception"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Clear-AuditPolicies {
|
|
<#
|
|
.SYNOPSIS
|
|
Clear all audit policies to disabled state
|
|
|
|
.DESCRIPTION
|
|
Uses auditpol.exe /clear to reset all audit policies to system defaults.
|
|
This is the official Microsoft method to clear audit policies.
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param()
|
|
|
|
try {
|
|
Write-Log -Level INFO -Message "Clearing all audit policies..." -Module "Rollback"
|
|
|
|
# Use auditpol /clear /y (official MS command)
|
|
# /clear: Deletes per-user policy, resets system policy, disables all auditing
|
|
# /y: Suppress confirmation prompt
|
|
$process = Start-Process -FilePath "auditpol.exe" `
|
|
-ArgumentList "/clear", "/y" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru `
|
|
-RedirectStandardOutput (Join-Path $env:TEMP "auditpol_clear_stdout.txt") `
|
|
-RedirectStandardError (Join-Path $env:TEMP "auditpol_clear_stderr.txt")
|
|
|
|
if ($process.ExitCode -eq 0) {
|
|
Write-Log -Level SUCCESS -Message "Audit policies cleared successfully" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
$errorOutput = Get-Content (Join-Path $env:TEMP "auditpol_clear_stderr.txt") -Raw -ErrorAction SilentlyContinue
|
|
Write-Log -Level ERROR -Message "Failed to clear audit policies: $errorOutput" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Exception clearing audit policies" -Module "Rollback" -Exception $_.Exception
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Clear-ASRRules {
|
|
<#
|
|
.SYNOPSIS
|
|
Clear all ASR rules to Not Configured state
|
|
|
|
.DESCRIPTION
|
|
Uses Remove-MpPreference to remove all ASR rule configurations.
|
|
This sets all rules back to "Not configured" state.
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param()
|
|
|
|
try {
|
|
Write-Log -Level INFO -Message "Clearing all ASR rules..." -Module "Rollback"
|
|
|
|
# Get current ASR rules
|
|
$mpPref = Get-MpPreference -ErrorAction Stop
|
|
|
|
if ($mpPref.AttackSurfaceReductionRules_Ids -and $mpPref.AttackSurfaceReductionRules_Ids.Count -gt 0) {
|
|
# Remove all ASR rule IDs and Actions
|
|
Remove-MpPreference -AttackSurfaceReductionRules_Ids $mpPref.AttackSurfaceReductionRules_Ids -ErrorAction Stop
|
|
Remove-MpPreference -AttackSurfaceReductionRules_Actions $mpPref.AttackSurfaceReductionRules_Actions -ErrorAction Stop
|
|
|
|
Write-Log -Level SUCCESS -Message "Cleared $($mpPref.AttackSurfaceReductionRules_Ids.Count) ASR rules" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "No ASR rules configured - nothing to clear" -Module "Rollback"
|
|
return $true
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log -Level ERROR -Message "Failed to clear ASR rules" -Module "Rollback" -Exception $_.Exception
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Reset-SecurityTemplate {
|
|
<#
|
|
.SYNOPSIS
|
|
Restore security template settings from pre-hardening backup
|
|
|
|
.DESCRIPTION
|
|
Uses secedit.exe to restore security template settings from the backed up state.
|
|
This includes password policies, user rights assignments, and other security settings.
|
|
Falls back to defltbase.inf if no backup exists (with warning about limitations).
|
|
|
|
.PARAMETER BackupFile
|
|
Path to the pre-hardening security policy .inf backup file
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$BackupFile
|
|
)
|
|
|
|
try {
|
|
$templateToUse = $null
|
|
$database = Join-Path $env:TEMP "secedit_restore.sdb"
|
|
$logFile = Join-Path $env:TEMP "secedit_restore.log"
|
|
|
|
# Check if backup file exists and use it
|
|
if ($BackupFile -and (Test-Path $BackupFile)) {
|
|
Write-Log -Level INFO -Message "Restoring security template from pre-hardening backup..." -Module "Rollback"
|
|
$templateToUse = $BackupFile
|
|
}
|
|
else {
|
|
# Fallback to defltbase.inf with warning
|
|
Write-Log -Level WARNING -Message "No pre-hardening backup found. Using defltbase.inf (may not reset all settings)" -Module "Rollback"
|
|
Write-Log -Level WARNING -Message "Microsoft KB 313222: defltbase.inf is no longer capable of resetting all security defaults" -Module "Rollback"
|
|
|
|
$defaultTemplate = "$env:WINDIR\inf\defltbase.inf"
|
|
|
|
if (-not (Test-Path $defaultTemplate)) {
|
|
Write-Log -Level ERROR -Message "Default security template not found: $defaultTemplate" -Module "Rollback"
|
|
return $false
|
|
}
|
|
|
|
$templateToUse = $defaultTemplate
|
|
}
|
|
|
|
# STEP 1: Import .inf file into database (required before configure)
|
|
# Import only securitypolicy and user_rights areas (we handle audit policies separately with auditpol)
|
|
Write-Log -Level INFO -Message "Importing security template into database..." -Module "Rollback"
|
|
$importProcess = Start-Process -FilePath "secedit.exe" `
|
|
-ArgumentList "/import", "/db", "`"$database`"", "/cfg", "`"$templateToUse`"", "/overwrite", "/areas", "securitypolicy", "user_rights", "/log", "`"$logFile`"", "/quiet" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru
|
|
|
|
if ($importProcess.ExitCode -ne 0) {
|
|
$errorLog = Get-Content $logFile -Raw -ErrorAction SilentlyContinue
|
|
Write-Log -Level ERROR -Message "Failed to import security template (Exit: $($importProcess.ExitCode)): $errorLog" -Module "Rollback"
|
|
Write-Log -Level ERROR -Message "Template file: $templateToUse" -Module "Rollback"
|
|
return $false
|
|
}
|
|
|
|
Write-Log -Level SUCCESS -Message "Security template imported successfully" -Module "Rollback"
|
|
|
|
# STEP 2: Configure system from database (only securitypolicy and user_rights)
|
|
Write-Log -Level INFO -Message "Applying security template to system..." -Module "Rollback"
|
|
$process = Start-Process -FilePath "secedit.exe" `
|
|
-ArgumentList "/configure", "/db", "`"$database`"", "/areas", "securitypolicy", "user_rights", "/log", "`"$logFile`"", "/quiet" `
|
|
-Wait `
|
|
-NoNewWindow `
|
|
-PassThru
|
|
|
|
$errorLog = Get-Content $logFile -Raw -ErrorAction SilentlyContinue
|
|
|
|
# Exit code evaluation:
|
|
# 0 = success
|
|
# 3 = success with warnings
|
|
# 1 = error, BUT if it's only SID-mapping issues, treat as success with warning
|
|
$isSidMappingOnly = $errorLog -match 'Zuordnungen von Kontennamen.*Sicherheitskennungen|account name.*security identifier'
|
|
|
|
if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3 -or ($process.ExitCode -eq 1 -and $isSidMappingOnly)) {
|
|
if ($process.ExitCode -eq 1) {
|
|
Write-Log -Level WARNING -Message "Security template restored with SID-mapping warnings (non-fatal, most settings applied)" -Module "Rollback"
|
|
}
|
|
|
|
if ($BackupFile) {
|
|
Write-Log -Level SUCCESS -Message "Security template restored from pre-hardening backup" -Module "Rollback"
|
|
}
|
|
else {
|
|
Write-Log -Level SUCCESS -Message "Security template reset using defltbase.inf (partial reset)" -Module "Rollback"
|
|
}
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level ERROR -Message "Failed to restore security template (Exit: $($process.ExitCode)): $errorLog" -Module "Rollback"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to restore security template from backup" -Module "Rollback" -ErrorRecord $_
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Clear-LocalGPO {
|
|
<#
|
|
.SYNOPSIS
|
|
Clear all local Group Policy settings to "Not Configured"
|
|
|
|
.DESCRIPTION
|
|
Deletes the registry.pol files which store local GPO settings.
|
|
This is the official Microsoft method to reset all GPO settings to default.
|
|
After deletion, gpupdate will recreate empty directories and all settings
|
|
will be "Not Configured".
|
|
|
|
Reference: https://woshub.com/reset-local-group-policies-settings-in-windows/
|
|
|
|
.OUTPUTS
|
|
Boolean indicating success
|
|
#>
|
|
[CmdletBinding()]
|
|
[OutputType([bool])]
|
|
param()
|
|
|
|
try {
|
|
Write-Log -Level INFO -Message "Clearing all local Group Policy settings..." -Module "Rollback"
|
|
|
|
# Paths to local GPO registry.pol files
|
|
$gpoPaths = @(
|
|
"$env:WinDir\System32\GroupPolicyUsers",
|
|
"$env:WinDir\System32\GroupPolicy"
|
|
)
|
|
|
|
$clearedCount = 0
|
|
|
|
foreach ($path in $gpoPaths) {
|
|
if (Test-Path $path) {
|
|
try {
|
|
Remove-Item -Path $path -Recurse -Force -ErrorAction Stop
|
|
Write-Log -Level INFO -Message "Deleted GPO directory: $path" -Module "Rollback"
|
|
$clearedCount++
|
|
}
|
|
catch {
|
|
Write-Log -Level WARNING -Message "Could not delete GPO directory: $path - $_" -Module "Rollback"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($clearedCount -gt 0) {
|
|
Write-Log -Level SUCCESS -Message "Local Group Policy cleared successfully" -Module "Rollback"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log -Level INFO -Message "No local GPO directories found to clear" -Module "Rollback"
|
|
return $true
|
|
}
|
|
}
|
|
catch {
|
|
Write-ErrorLog -Message "Failed to clear local Group Policy Objects" -Module "Rollback" -ErrorRecord $_
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Note: Export-ModuleMember not used - this script is dot-sourced, not imported as module
|