feat(registry): add GPO override warning and WhatIf dry-run previews (#611)

Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
This commit is contained in:
HetCreep
2026-06-22 02:30:31 +07:00
committed by GitHub
parent 82894176d9
commit dfe7810346
16 changed files with 178 additions and 32 deletions

View File

@@ -25,6 +25,15 @@ function RemoveApps {
$appslist $appslist
) )
if ($script:Params.ContainsKey("WhatIf")) {
foreach ($app in $appslist) {
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
}
Write-Host ""
return
}
# Determine target from script-level params, defaulting to AllUsers # Determine target from script-level params, defaulting to AllUsers
$targetUser = GetTargetUserForAppRemoval $targetUser = GetTargetUserForAppRemoval

View File

@@ -77,7 +77,9 @@ function ExecuteParameter {
'DisableWidgets' { 'DisableWidgets' {
Write-Host "> $($feature.ApplyText)..." Write-Host "> $($feature.ApplyText)..."
# Stop widgets related processes before removing the app packages to prevent potential issues # Stop widgets related processes before removing the app packages to prevent potential issues
if (-not $script:Params.ContainsKey("WhatIf")) {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
}
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime') RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
} }
@@ -185,6 +187,10 @@ function ExecuteAllChanges {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..." & $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
} }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Create registry backup" -ForegroundColor Cyan
}
else {
Write-Host "> Creating registry backup..." Write-Host "> Creating registry backup..."
try { try {
$undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object { $undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object {
@@ -199,6 +205,7 @@ function ExecuteAllChanges {
throw "Registry backup failed before applying changes. $($_.Exception.Message)" throw "Registry backup failed before applying changes. $($_.Exception.Message)"
} }
} }
}
# Create restore point if requested (CLI only - GUI handles this separately) # Create restore point if requested (CLI only - GUI handles this separately)
if ($script:Params.ContainsKey("CreateRestorePoint")) { if ($script:Params.ContainsKey("CreateRestorePoint")) {
@@ -206,10 +213,16 @@ function ExecuteAllChanges {
if ($script:ApplyProgressCallback) { if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..." & $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
} }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan
Write-Host ""
}
else {
Write-Host "> Creating a system restore point..." Write-Host "> Creating a system restore point..."
CreateSystemRestorePoint CreateSystemRestorePoint
Write-Host "" Write-Host ""
} }
}
# Execute all parameters # Execute all parameters
foreach ($paramKey in $actionableKeys) { foreach ($paramKey in $actionableKeys) {

View File

@@ -21,6 +21,12 @@ function ImportRegistryFile {
$importScript = { $importScript = {
param($targetRegFilePath, $hiveContext) param($targetRegFilePath, $hiveContext)
if ($script:Params.ContainsKey("WhatIf")) {
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host ""
return
}
# When the target user's hive is already loaded under their SID, the .reg file's # When the target user's hive is already loaded under their SID, the .reg file's
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead, # HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
# which remaps Default → SID via Split-RegistryPath. # which remaps Default → SID via Split-RegistryPath.

View File

@@ -26,6 +26,11 @@ function ReplaceStartMenuForAllUsers {
# Also replace the start menu file for the default user profile # Also replace the start menu file for the default user profile
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false $defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
return
}
# Create folder if it doesn't exist # Create folder if it doesn't exist
if (-not (Test-Path $defaultStartMenuPath)) { if (-not (Test-Path $defaultStartMenuPath)) {
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null
@@ -61,6 +66,11 @@ function ReplaceStartMenu {
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile $userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
return
}
$backupBinFile = $startMenuBinFile + ".bak" $backupBinFile = $startMenuBinFile + ".bak"
if (Test-Path $startMenuBinFile) { if (Test-Path $startMenuBinFile) {
@@ -121,6 +131,15 @@ function RestoreStartMenuFromBackup {
} }
$currentBinBackup = $StartMenuBinFile + '.restore.bak' $currentBinBackup = $StartMenuBinFile + '.restore.bak'
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "[WhatIf] Restored start menu for user $userName."
}
}
if (-not (Test-Path -LiteralPath $backupBinFile)) { if (-not (Test-Path -LiteralPath $backupBinFile)) {
return [PSCustomObject]@{ return [PSCustomObject]@{
UserName = $userName UserName = $userName
@@ -184,6 +203,15 @@ function RestoreStartMenuForAllUsers {
if (Test-Path $defaultStartMenuPath) { if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin' $defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
if (Test-Path -LiteralPath $defaultStartMenuBinFile) { if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Remove start2.bin for the default user profile" -ForegroundColor Cyan
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = '[WhatIf] Removed start2.bin for the default user profile.'
}
}
else {
try { try {
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
$results += [PSCustomObject]@{ $results += [PSCustomObject]@{
@@ -201,6 +229,7 @@ function RestoreStartMenuForAllUsers {
} }
} }
} }
}
if ($results.Count -eq 0) { if ($results.Count -eq 0) {
$results += [PSCustomObject]@{ $results += [PSCustomObject]@{

View File

@@ -5,6 +5,11 @@ function RestartExplorer {
return return
} }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
return
}
Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..." Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..."
if ($script:Params.ContainsKey("NoRestartExplorer")) { if ($script:Params.ContainsKey("NoRestartExplorer")) {

View File

@@ -133,6 +133,11 @@ function Restore-RegistryBackupState {
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target) $friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
return [PSCustomObject]@{ Result = $true }
}
$restoreAction = { $restoreAction = {
param($normalizedBackup) param($normalizedBackup)
@@ -148,9 +153,10 @@ function Restore-RegistryBackupState {
Write-Host "Restore requires loading target user hive." Write-Host "Restore requires loading target user hive."
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
Write-Host "Restore completed for $friendlyTarget." Write-Host "Restore completed for $friendlyTarget."
return return [PSCustomObject]@{ Result = $true }
} }
& $restoreAction $Backup & $restoreAction $Backup
Write-Host "Restore completed for $friendlyTarget." Write-Host "Restore completed for $friendlyTarget."
return [PSCustomObject]@{ Result = $true }
} }

View File

@@ -54,6 +54,11 @@ function DisableStoreSearchSuggestions {
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' } if (-not $userName) { $userName = '<unknown>' }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return
}
# This file doesn't exist in EEA (No Store app suggestions). # This file doesn't exist in EEA (No Store app suggestions).
if (-not (Test-Path -Path $StoreAppsDatabase)) if (-not (Test-Path -Path $StoreAppsDatabase))
{ {
@@ -132,6 +137,11 @@ function EnableStoreSearchSuggestions {
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' } if (-not $userName) { $userName = '<unknown>' }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return
}
if (-not (Test-Path -Path $StoreAppsDatabase)) { if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Store app database not found for user $userName, nothing to undo" Write-Host "Store app database not found for user $userName, nothing to undo"
return return
@@ -245,7 +255,8 @@ function Test-StoreSearchSuggestionsDisabled {
$isEveryone = $false $isEveryone = $false
try { try {
$isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid $isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid
} catch { } }
catch { }
if ($isEveryone) { if ($isEveryone) {
return $true return $true

View File

@@ -4,6 +4,12 @@ function EnableWindowsFeature {
[string]$FeatureName [string]$FeatureName
) )
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host ""
return
}
$result = Invoke-NonBlocking -ScriptBlock { $result = Invoke-NonBlocking -ScriptBlock {
param($name) param($name)
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
@@ -21,6 +27,12 @@ function DisableWindowsFeature {
[string]$FeatureName [string]$FeatureName
) )
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
Write-Host ""
return
}
$result = Invoke-NonBlocking -ScriptBlock { $result = Invoke-NonBlocking -ScriptBlock {
param($name) param($name)
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart

View File

@@ -4,6 +4,11 @@ function SaveCustomAppsListToFile {
$appsList $appsList
) )
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Save custom apps list to file" -ForegroundColor Cyan
return
}
$script:SelectedApps = $appsList $script:SelectedApps = $appsList
# Create file that stores selected apps if it doesn't exist # Create file that stores selected apps if it doesn't exist

View File

@@ -1,5 +1,10 @@
# Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file # Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file
function SaveSettings { function SaveSettings {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Save settings to LastUsedSettings.json" -ForegroundColor Cyan
return
}
$settings = @{ $settings = @{
"Version" = "1.0" "Version" = "1.0"
"Settings" = @() "Settings" = @()

View File

@@ -21,7 +21,8 @@ function Show-ImportExportConfigWindow {
$Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
} }
} }
} catch { } }
catch { }
# Load XAML from schema file # Load XAML from schema file
$schemaPath = $script:ImportExportConfigSchema $schemaPath = $script:ImportExportConfigSchema
@@ -52,7 +53,8 @@ function Show-ImportExportConfigWindow {
if ($mainCheckBoxStyle) { if ($mainCheckBoxStyle) {
$dlg.Resources.Add([type][System.Windows.Controls.CheckBox], $mainCheckBoxStyle) $dlg.Resources.Add([type][System.Windows.Controls.CheckBox], $mainCheckBoxStyle)
} }
} catch { } }
catch { }
# Populate named elements # Populate named elements
$dlg.Title = $Title $dlg.Title = $Title
@@ -419,6 +421,12 @@ function Export-Configuration {
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))" Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan
Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
return
}
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) { if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)" Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null

View File

@@ -953,6 +953,14 @@
}) })
$window.Show() | Out-Null $window.Show() | Out-Null
# If WhatIf mode is enabled, notify the user that no changes will be made
if ($script:Params.ContainsKey("WhatIf")) {
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-MessageBox -Message "WhatIf mode is enabled. The script will not make any changes to your system in this mode.`n`nYou can observe the actions that would be taken by the script in the console output." -Title 'WhatIf Mode' -Button 'OK' -Icon 'Information' -Owner $window
}) | Out-Null
}
[System.Windows.Threading.Dispatcher]::PushFrame($frame) [System.Windows.Threading.Dispatcher]::PushFrame($frame)
return $null return $null
} }

View File

@@ -27,10 +27,17 @@ function Show-RestoreBackupWindow {
} }
Write-Host "User confirmed registry restore for $($backup.Target)." Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup $restoreOpResult = Restore-RegistryBackupState -Backup $backup
if ($restoreOpResult -and $restoreOpResult.Result) {
$restoreResult.RestoredRegistry = $true $restoreResult.RestoredRegistry = $true
if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Registry backup would be restored (no changes made).'
}
else {
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.' $successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
} }
}
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') { elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope $scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true) $useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
@@ -67,7 +74,10 @@ function Show-RestoreBackupWindow {
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary" $warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
} }
else { else {
if ($scope -eq 'AllUsers') { if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Start Menu backup would be restored (no changes made).'
}
elseif ($scope -eq 'AllUsers') {
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in." $successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
} }
else { else {

View File

@@ -203,6 +203,11 @@ function Invoke-RegistryOperationsFromRegFile {
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath) $operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
$totalOperations = $operations.Count $totalOperations = $operations.Count
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan
return
}
foreach ($operation in $operations) { foreach ($operation in $operations) {
try { try {
Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath

View File

@@ -81,3 +81,4 @@ function Get-RegistryFilePathForFeature {
return Join-Path $script:RegfilesPath $RegistryKey return Join-Path $script:RegfilesPath $RegistryKey
} }

View File

@@ -221,6 +221,15 @@ else {
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
} }
# Check if the device is domain-joined and warn the user (Group Policy may override changes)
try {
$computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) {
Write-Warning "This machine is domain-joined. Group Policy may override changes made by Win11Debloat."
}
}
catch { }
# Check if script has all required files # Check if script has all required files
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) { if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) {
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present" Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
@@ -494,7 +503,11 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
try { try {
$result = Show-MainWindow $result = Show-MainWindow
try {
Stop-Transcript Stop-Transcript
}
catch { }
Exit Exit
} }
catch { catch {