Improve registry backup safety and add optional backup skipping (#710)

This commit is contained in:
Jeffrey
2026-07-25 20:05:49 +02:00
committed by GitHub
parent 68cacfce89
commit 32cedaf65d
30 changed files with 1603 additions and 43 deletions
@@ -201,7 +201,8 @@ function Get-RegistryKeySnapshot {
.DESCRIPTION
Captures all values or selected value names, records missing selected values,
and recursively captures subkeys when requested.
and recursively captures subkeys when requested. Throws if a requested subkey
cannot be read.
#>
function Convert-RegistryKeyToSnapshot {
param(
@@ -241,7 +242,9 @@ function Convert-RegistryKeyToSnapshot {
if ($IncludeSubKeys) {
foreach ($subKeyName in @($RegistryKey.GetSubKeyNames())) {
$childKey = $RegistryKey.OpenSubKey($subKeyName, $false)
if ($null -eq $childKey) { continue }
if ($null -eq $childKey) {
throw "Unable to read registry subkey '$($RegistryKey.Name)\$subKeyName' while creating a backup snapshot. The backup was not created."
}
try {
$childPath = if ([string]::IsNullOrWhiteSpace($FullPath)) { $subKeyName } else { "$FullPath\$subKeyName" }
+6 -6
View File
@@ -121,7 +121,7 @@ function Invoke-FeatureApply {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey("Sysprep")) {
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
DisableStoreSearchSuggestionsForAllUsers
Set-StoreSearchSuggestionsDisabledForAllUsers
Write-Host ""
return
}
@@ -159,7 +159,7 @@ function Invoke-FeatureUndo {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
EnableStoreSearchSuggestionsForAllUsers
Set-StoreSearchSuggestionsEnabledForAllUsers
Write-Host ""
return
}
@@ -302,8 +302,8 @@ function Invoke-UndoFeatures {
.DESCRIPTION
Sequenced in four phases:
1. Registry backup
2. System restore point
1. Registry backup (skipped when SkipRegistryBackup is present)
2. System restore point (skipped when CreateRestorePoint is absent)
3. Apply phase - applies all selected features via Invoke-ApplyFeatures
4. Undo phase - undoes selected features via Invoke-UndoFeatures
@@ -349,14 +349,14 @@ function Invoke-AllChanges {
# ---- Calculate total progress steps ----
$totalSteps = $applyIds.Count + $undoIds.Count
if ($needsBackup) { $totalSteps++ }
if ($needsBackup -and -not $script:Params.ContainsKey('SkipRegistryBackup')) { $totalSteps++ }
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$step = 0
# ================================================================
# Phase 1: Registry backup
# ================================================================
if ($needsBackup) {
if ($needsBackup -and -not $script:Params.ContainsKey('SkipRegistryBackup')) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
@@ -65,17 +65,103 @@ function Restore-RegistryKeySnapshot {
throw "Unsupported root-level registry path in backup: $($Snapshot.Path)"
}
Test-RegistryKeySnapshotCanBeRestored -Snapshot $Snapshot
Restore-RegistryKeySnapshotAtPath -Snapshot $Snapshot -RootKey $rootKey -SubKeyPath $subKeyPath
}
<#
.SYNOPSIS
Validates registry values and subkey paths in a snapshot before live registry state is changed.
.PARAMETER Snapshot
The registry key snapshot to validate before it is restored.
#>
function Test-RegistryKeySnapshotCanBeRestored {
param(
[Parameter(Mandatory)]
$Snapshot
)
if (-not [bool]$Snapshot.Exists) { return }
$childNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($valueSnapshot in @($Snapshot.Values)) {
if ([bool]$valueSnapshot.Exists) {
$valueKind = Convert-RegistryValueKindFromBackup -KindName $valueSnapshot.Kind
$null = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $valueSnapshot.Data
}
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
$childName = Get-DirectRegistrySnapshotChildName -ParentPath $Snapshot.Path -ChildPath $subKeySnapshot.Path
if ([string]::IsNullOrWhiteSpace($childName) -or -not $childNames.Add($childName)) {
throw "Backup contains duplicate or unsupported registry child path: $($subKeySnapshot.Path)"
}
Test-RegistryKeySnapshotCanBeRestored -Snapshot $subKeySnapshot
}
}
<#
.SYNOPSIS
Returns a snapshot child's name only when it is directly below its parent.
.PARAMETER ParentPath
The registry path of the expected parent snapshot.
.PARAMETER ChildPath
The registry path of the child snapshot to validate.
#>
function Get-DirectRegistrySnapshotChildName {
param(
[Parameter(Mandatory)]
[string]$ParentPath,
[Parameter(Mandatory)]
[string]$ChildPath
)
$parentParts = Split-RegistryPath -path $ParentPath
$childParts = Split-RegistryPath -path $ChildPath
if (-not $parentParts -or -not $childParts -or
-not $parentParts.Hive.Equals($childParts.Hive, [System.StringComparison]::OrdinalIgnoreCase) -or
[string]::IsNullOrWhiteSpace($parentParts.SubKey) -or
[string]::IsNullOrWhiteSpace($childParts.SubKey)) {
throw "Unsupported registry child path in backup: $ChildPath"
}
$childName = Split-Path -Path $childParts.SubKey -Leaf
$expectedSubKey = "$($parentParts.SubKey)\$childName"
if ([string]::IsNullOrWhiteSpace($childName) -or
-not $childParts.SubKey.Equals($expectedSubKey, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Registry child path '$ChildPath' is not directly below parent '$ParentPath'."
}
return $childName
}
<#
.SYNOPSIS
Restores a snapshot to a specific path below an already resolved registry root.
.DESCRIPTION
Writes only values and descendants represented by the backup. Existing keys are
retained so their security descriptors and unrelated data are not destroyed.
#>
function Restore-RegistryKeySnapshotAtPath {
param(
[Parameter(Mandatory)]
$Snapshot,
[Parameter(Mandatory)]
$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
if (-not $Snapshot.Exists) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
Remove-RegistrySubKeyTreeIfExists -RootKey $RootKey -SubKeyPath $SubKeyPath
return
}
$forceFullTree = @($Snapshot.SubKeys).Count -gt 0
if ($forceFullTree) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
}
$key = $rootKey.CreateSubKey($subKeyPath)
$key = $RootKey.CreateSubKey($SubKeyPath)
if ($null -eq $key) {
throw "Unable to create or open registry key '$($Snapshot.Path)'"
}
@@ -90,8 +176,11 @@ function Restore-RegistryKeySnapshot {
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
Restore-RegistryKeySnapshot -Snapshot $subKeySnapshot
$childName = Get-DirectRegistrySnapshotChildName -ParentPath $Snapshot.Path -ChildPath $subKeySnapshot.Path
Restore-RegistryKeySnapshotAtPath -Snapshot $subKeySnapshot -RootKey $RootKey -SubKeyPath "$SubKeyPath\$childName"
}
}
<#
@@ -219,6 +219,11 @@ function Get-DeploymentSettings {
$deploySettings += @{ Name = 'CreateRestorePoint'; Value = [bool]$restorePointCheckBox.IsChecked }
}
$registryBackupCheckBox = $Owner.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox) {
$deploySettings += @{ Name = 'SkipRegistryBackup'; Value = -not [bool]$registryBackupCheckBox.IsChecked }
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($restartExplorerCheckBox) {
$deploySettings += @{ Name = 'RestartExplorer'; Value = [bool]$restartExplorerCheckBox.IsChecked }
@@ -272,6 +277,7 @@ function Get-DeploymentCategoryDetailString {
$options = @()
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += 'Restore Point' }
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += 'Registry Backup' }
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += 'Restart Explorer' }
$lines = @()
@@ -378,6 +384,13 @@ function Set-ImportedDeploymentSettings {
$restorePointCheckBox.IsChecked = [bool]$lookup['CreateRestorePoint']
}
$registryBackupCheckBox = $Owner.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox) {
if ($lookup.ContainsKey('SkipRegistryBackup')) {
$registryBackupCheckBox.IsChecked = -not [bool]$lookup['SkipRegistryBackup']
}
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($lookup.ContainsKey('RestartExplorer') -and $restartExplorerCheckBox) {
$restartExplorerCheckBox.IsChecked = [bool]$lookup['RestartExplorer']
+11
View File
@@ -702,6 +702,11 @@ function Show-MainWindow {
Add-Parameter 'CreateRestorePoint'
}
$registryBackupCheckBox = $window.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox -and -not $registryBackupCheckBox.IsChecked) {
Add-Parameter 'SkipRegistryBackup'
}
switch ($userSelectionCombo.SelectedIndex) {
0 { Write-Host "Selected user mode: current user ($(Get-UserName))" }
1 {
@@ -782,6 +787,12 @@ function Show-MainWindow {
$restartExplorerCheckBox.IsEnabled = $false
}
$registryBackupCheckBox = $window.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox -and $script:Params.ContainsKey('SkipRegistryBackup')) {
$registryBackupCheckBox.IsChecked = $false
$registryBackupCheckBox.IsEnabled = $false
}
if ($script:Params.ContainsKey("Sysprep")) {
$userSelectionCombo.SelectedIndex = 2
$userSelectionCombo.IsEnabled = $false
+1
View File
@@ -10,6 +10,7 @@ param (
[Alias('NoRestartExplorer')]
[switch]$SkipExplorerRestart,
[switch]$CreateRestorePoint,
[switch]$SkipRegistryBackup,
[switch]$RunDefaults,
[switch]$RunDefaultsLite,
[switch]$RunSavedSettings,
@@ -81,6 +81,11 @@ function Import-ConfigToParams {
$importedItems++
}
if ($deploymentLookup.ContainsKey('SkipRegistryBackup') -and [bool]$deploymentLookup['SkipRegistryBackup']) {
Add-Parameter 'SkipRegistryBackup'
$importedItems++
}
if ($deploymentLookup.ContainsKey('RestartExplorer') -and -not [bool]$deploymentLookup['RestartExplorer']) {
Add-Parameter 'SkipExplorerRestart'
$importedItems++