From dfe7810346b8f2aca21e677701cdc6ec3b3be044 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Mon, 22 Jun 2026 02:30:31 +0700 Subject: [PATCH] feat(registry): add GPO override warning and WhatIf dry-run previews (#611) Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com> --- Scripts/AppRemoval/RemoveApps.ps1 | 9 ++++ Scripts/Features/ExecuteChanges.ps1 | 43 ++++++++++++------- Scripts/Features/ImportRegistryFile.ps1 | 6 +++ Scripts/Features/ReplaceStartMenu.ps1 | 45 ++++++++++++++++---- Scripts/Features/RestartExplorer.ps1 | 5 +++ Scripts/Features/RestoreRegistryBackup.ps1 | 8 +++- Scripts/Features/StoreSearchSuggestions.ps1 | 13 +++++- Scripts/Features/WindowsOptionalFeatures.ps1 | 12 ++++++ Scripts/FileIO/SaveCustomAppsListToFile.ps1 | 5 +++ Scripts/FileIO/SaveSettings.ps1 | 5 +++ Scripts/GUI/Show-ConfigWindow.ps1 | 12 +++++- Scripts/GUI/Show-MainWindow.ps1 | 8 ++++ Scripts/GUI/Show-RestoreBackupWindow.ps1 | 18 ++++++-- Scripts/Helpers/ApplyRegistryRegFile.ps1 | 5 +++ Scripts/Helpers/RegistryPathHelpers.ps1 | 1 + Win11Debloat.ps1 | 15 ++++++- 16 files changed, 178 insertions(+), 32 deletions(-) diff --git a/Scripts/AppRemoval/RemoveApps.ps1 b/Scripts/AppRemoval/RemoveApps.ps1 index 3a729ba..8d31296 100644 --- a/Scripts/AppRemoval/RemoveApps.ps1 +++ b/Scripts/AppRemoval/RemoveApps.ps1 @@ -25,6 +25,15 @@ function RemoveApps { $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 $targetUser = GetTargetUserForAppRemoval diff --git a/Scripts/Features/ExecuteChanges.ps1 b/Scripts/Features/ExecuteChanges.ps1 index 1981f1f..c9b2d6b 100644 --- a/Scripts/Features/ExecuteChanges.ps1 +++ b/Scripts/Features/ExecuteChanges.ps1 @@ -77,7 +77,9 @@ function ExecuteParameter { 'DisableWidgets' { Write-Host "> $($feature.ApplyText)..." # Stop widgets related processes before removing the app packages to prevent potential issues - Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process + if (-not $script:Params.ContainsKey("WhatIf")) { + Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process + } RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime') } @@ -185,18 +187,23 @@ function ExecuteAllChanges { & $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..." } - Write-Host "> Creating registry backup..." - try { - $undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object { - $f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null } - if ($f -and $f.RegistryUndoKey) { - [PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) } - } - } | Where-Object { $_ }) - New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Create registry backup" -ForegroundColor Cyan } - catch { - throw "Registry backup failed before applying changes. $($_.Exception.Message)" + else { + Write-Host "> Creating registry backup..." + try { + $undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object { + $f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null } + if ($f -and $f.RegistryUndoKey) { + [PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) } + } + } | Where-Object { $_ }) + New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null + } + catch { + throw "Registry backup failed before applying changes. $($_.Exception.Message)" + } } } @@ -206,9 +213,15 @@ function ExecuteAllChanges { if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..." } - Write-Host "> Creating a system restore point..." - CreateSystemRestorePoint - Write-Host "" + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan + Write-Host "" + } + else { + Write-Host "> Creating a system restore point..." + CreateSystemRestorePoint + Write-Host "" + } } # Execute all parameters diff --git a/Scripts/Features/ImportRegistryFile.ps1 b/Scripts/Features/ImportRegistryFile.ps1 index 1bfb73c..2524280 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -21,6 +21,12 @@ function ImportRegistryFile { $importScript = { 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 # HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead, # which remaps Default → SID via Split-RegistryPath. diff --git a/Scripts/Features/ReplaceStartMenu.ps1 b/Scripts/Features/ReplaceStartMenu.ps1 index 94f3e88..ede5254 100644 --- a/Scripts/Features/ReplaceStartMenu.ps1 +++ b/Scripts/Features/ReplaceStartMenu.ps1 @@ -26,6 +26,11 @@ function ReplaceStartMenuForAllUsers { # 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 + 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 if (-not (Test-Path $defaultStartMenuPath)) { new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null @@ -61,6 +66,11 @@ function ReplaceStartMenu { $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" if (Test-Path $startMenuBinFile) { @@ -121,6 +131,15 @@ function RestoreStartMenuFromBackup { } $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)) { return [PSCustomObject]@{ UserName = $userName @@ -184,19 +203,29 @@ function RestoreStartMenuForAllUsers { if (Test-Path $defaultStartMenuPath) { $defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin' if (Test-Path -LiteralPath $defaultStartMenuBinFile) { - try { - Remove-Item -LiteralPath $defaultStartMenuBinFile -Force + 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 = 'Removed start2.bin for the default user profile.' + Message = '[WhatIf] Removed start2.bin for the default user profile.' } } - catch { - $results += [PSCustomObject]@{ - UserName = 'Default' - Result = $false - Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)" + else { + try { + Remove-Item -LiteralPath $defaultStartMenuBinFile -Force + $results += [PSCustomObject]@{ + UserName = 'Default' + Result = $true + Message = 'Removed start2.bin for the default user profile.' + } + } + catch { + $results += [PSCustomObject]@{ + UserName = 'Default' + Result = $false + Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)" + } } } } diff --git a/Scripts/Features/RestartExplorer.ps1 b/Scripts/Features/RestartExplorer.ps1 index eff2bc2..b6c7da1 100644 --- a/Scripts/Features/RestartExplorer.ps1 +++ b/Scripts/Features/RestartExplorer.ps1 @@ -5,6 +5,11 @@ function RestartExplorer { 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..." if ($script:Params.ContainsKey("NoRestartExplorer")) { diff --git a/Scripts/Features/RestoreRegistryBackup.ps1 b/Scripts/Features/RestoreRegistryBackup.ps1 index 002fbb0..0d56269 100644 --- a/Scripts/Features/RestoreRegistryBackup.ps1 +++ b/Scripts/Features/RestoreRegistryBackup.ps1 @@ -133,6 +133,11 @@ function Restore-RegistryBackupState { $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 = { param($normalizedBackup) @@ -148,9 +153,10 @@ function Restore-RegistryBackupState { Write-Host "Restore requires loading target user hive." Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup Write-Host "Restore completed for $friendlyTarget." - return + return [PSCustomObject]@{ Result = $true } } & $restoreAction $Backup Write-Host "Restore completed for $friendlyTarget." + return [PSCustomObject]@{ Result = $true } } diff --git a/Scripts/Features/StoreSearchSuggestions.ps1 b/Scripts/Features/StoreSearchSuggestions.ps1 index 2633ba3..8054951 100644 --- a/Scripts/Features/StoreSearchSuggestions.ps1 +++ b/Scripts/Features/StoreSearchSuggestions.ps1 @@ -54,6 +54,11 @@ function DisableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value if (-not $userName) { $userName = '' } + 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). if (-not (Test-Path -Path $StoreAppsDatabase)) { @@ -132,6 +137,11 @@ function EnableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value if (-not $userName) { $userName = '' } + 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)) { Write-Host "Store app database not found for user $userName, nothing to undo" return @@ -245,7 +255,8 @@ function Test-StoreSearchSuggestionsDisabled { $isEveryone = $false try { $isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid - } catch { } + } + catch { } if ($isEveryone) { return $true diff --git a/Scripts/Features/WindowsOptionalFeatures.ps1 b/Scripts/Features/WindowsOptionalFeatures.ps1 index f1361c3..3c9fdc5 100644 --- a/Scripts/Features/WindowsOptionalFeatures.ps1 +++ b/Scripts/Features/WindowsOptionalFeatures.ps1 @@ -4,6 +4,12 @@ function EnableWindowsFeature { [string]$FeatureName ) + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan + Write-Host "" + return + } + $result = Invoke-NonBlocking -ScriptBlock { param($name) Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart @@ -21,6 +27,12 @@ function DisableWindowsFeature { [string]$FeatureName ) + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan + Write-Host "" + return + } + $result = Invoke-NonBlocking -ScriptBlock { param($name) Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart diff --git a/Scripts/FileIO/SaveCustomAppsListToFile.ps1 b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 index 7ac8bdf..e01f315 100644 --- a/Scripts/FileIO/SaveCustomAppsListToFile.ps1 +++ b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 @@ -4,6 +4,11 @@ function SaveCustomAppsListToFile { $appsList ) + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Save custom apps list to file" -ForegroundColor Cyan + return + } + $script:SelectedApps = $appsList # Create file that stores selected apps if it doesn't exist diff --git a/Scripts/FileIO/SaveSettings.ps1 b/Scripts/FileIO/SaveSettings.ps1 index 730808b..2548d6e 100644 --- a/Scripts/FileIO/SaveSettings.ps1 +++ b/Scripts/FileIO/SaveSettings.ps1 @@ -1,5 +1,10 @@ # Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file function SaveSettings { + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Save settings to LastUsedSettings.json" -ForegroundColor Cyan + return + } + $settings = @{ "Version" = "1.0" "Settings" = @() diff --git a/Scripts/GUI/Show-ConfigWindow.ps1 b/Scripts/GUI/Show-ConfigWindow.ps1 index efae6f2..82fb826 100644 --- a/Scripts/GUI/Show-ConfigWindow.ps1 +++ b/Scripts/GUI/Show-ConfigWindow.ps1 @@ -21,7 +21,8 @@ function Show-ImportExportConfigWindow { $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) } } - } catch { } + } + catch { } # Load XAML from schema file $schemaPath = $script:ImportExportConfigSchema @@ -52,7 +53,8 @@ function Show-ImportExportConfigWindow { if ($mainCheckBoxStyle) { $dlg.Resources.Add([type][System.Windows.Controls.CheckBox], $mainCheckBoxStyle) } - } catch { } + } + catch { } # Populate named elements $dlg.Title = $Title @@ -419,6 +421,12 @@ function Export-Configuration { 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) { Write-Host "Configuration exported successfully: $($saveDialog.FileName)" Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 index d4771ee..afcac82 100644 --- a/Scripts/GUI/Show-MainWindow.ps1 +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -953,6 +953,14 @@ }) $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) return $null } diff --git a/Scripts/GUI/Show-RestoreBackupWindow.ps1 b/Scripts/GUI/Show-RestoreBackupWindow.ps1 index dc9fadd..f8e0995 100644 --- a/Scripts/GUI/Show-RestoreBackupWindow.ps1 +++ b/Scripts/GUI/Show-RestoreBackupWindow.ps1 @@ -27,9 +27,16 @@ function Show-RestoreBackupWindow { } Write-Host "User confirmed registry restore for $($backup.Target)." - Restore-RegistryBackupState -Backup $backup - $restoreResult.RestoredRegistry = $true - $successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.' + $restoreOpResult = Restore-RegistryBackupState -Backup $backup + if ($restoreOpResult -and $restoreOpResult.Result) { + $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.' + } + } } elseif ($dialogResult.Result -eq 'RestoreStartMenu') { $scope = $dialogResult.StartMenuScope @@ -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" } 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." } else { diff --git a/Scripts/Helpers/ApplyRegistryRegFile.ps1 b/Scripts/Helpers/ApplyRegistryRegFile.ps1 index 6583a24..0185c6c 100644 --- a/Scripts/Helpers/ApplyRegistryRegFile.ps1 +++ b/Scripts/Helpers/ApplyRegistryRegFile.ps1 @@ -203,6 +203,11 @@ function Invoke-RegistryOperationsFromRegFile { $operations = @(Get-RegFileOperations -regFilePath $RegFilePath) $totalOperations = $operations.Count + if ($script:Params.ContainsKey("WhatIf")) { + Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan + return + } + foreach ($operation in $operations) { try { Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath diff --git a/Scripts/Helpers/RegistryPathHelpers.ps1 b/Scripts/Helpers/RegistryPathHelpers.ps1 index 683fd49..d666646 100644 --- a/Scripts/Helpers/RegistryPathHelpers.ps1 +++ b/Scripts/Helpers/RegistryPathHelpers.ps1 @@ -81,3 +81,4 @@ function Get-RegistryFilePathForFeature { return Join-Path $script:RegfilesPath $RegistryKey } + diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index c9694c9..c0270b2 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -221,6 +221,15 @@ else { 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 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" @@ -494,7 +503,11 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa try { $result = Show-MainWindow - Stop-Transcript + try { + Stop-Transcript + } + catch { } + Exit } catch {