From ca2879258b290542cb2b1533dcd1aa4a7b256965 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Tue, 9 Jun 2026 00:43:05 +0700 Subject: [PATCH 1/4] feat(registry): add GPO override alerts and WhatIf dry-run previews --- Scripts/Features/ImportRegistryFile.ps1 | 7 +++ Scripts/Helpers/ApplyRegistryRegFile.ps1 | 73 +++++++++++++++++++----- Scripts/Helpers/RegistryPathHelpers.ps1 | 56 ++++++++++++++++++ 3 files changed, 122 insertions(+), 14 deletions(-) diff --git a/Scripts/Features/ImportRegistryFile.ps1 b/Scripts/Features/ImportRegistryFile.ps1 index 1bfb73c3..de083321 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -21,6 +21,13 @@ function ImportRegistryFile { $importScript = { param($targetRegFilePath, $hiveContext) + if ($WhatIfPreference) { + Write-Host "[WhatIf] Previewing registry changes for $path:" -ForegroundColor Cyan + Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath -DryRun:$true + 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/Helpers/ApplyRegistryRegFile.ps1 b/Scripts/Helpers/ApplyRegistryRegFile.ps1 index 6583a243..785d96ef 100644 --- a/Scripts/Helpers/ApplyRegistryRegFile.ps1 +++ b/Scripts/Helpers/ApplyRegistryRegFile.ps1 @@ -100,18 +100,24 @@ function Invoke-RegistryDeleteValueOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - $KeyInfo + $KeyInfo, + [switch]$DryRun ) + $valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName + $displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName } + + if ($DryRun) { + Write-Host "[WhatIf] Remove Registry Value: $($Operation.KeyPath) \ $displayValueName" -ForegroundColor Cyan + return + } + if ($null -eq $KeyInfo.Key) { - $valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName - $displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName } Write-Verbose "Unable to find or open key '$($Operation.KeyPath)' and value '$displayValueName'" return } try { - $valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName $KeyInfo.Key.DeleteValue($valueName, $false) } finally { @@ -124,15 +130,26 @@ function Invoke-RegistrySetValueOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - $KeyInfo + $KeyInfo, + [switch]$DryRun ) + $setArgs = Convert-RegOperationToValueKind -Operation $Operation + if ($DryRun) { + $displayVal = if ($setArgs.Kind -eq [Microsoft.Win32.RegistryValueKind]::Binary) { + "Binary data ($($setArgs.Value.Length) bytes)" + } else { + $setArgs.Value + } + Write-Host "[WhatIf] Set Registry Value: $($Operation.KeyPath) \ $($setArgs.Name) = $displayVal ($($setArgs.Kind))" -ForegroundColor Cyan + return + } + if ($null -eq $KeyInfo.Key) { throw [System.UnauthorizedAccessException]::new("Unable to open or create registry key '$($Operation.KeyPath)'") } try { - $setArgs = Convert-RegOperationToValueKind -Operation $Operation $KeyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind) } finally { @@ -166,13 +183,38 @@ function Invoke-RegistryOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - [string]$RegFilePath + [string]$RegFilePath, + [switch]$DryRun ) $operationType = [string]$Operation.OperationType $isSetValueOperation = $operationType -eq 'SetValue' $isDeleteKeyOperation = $operationType -eq 'DeleteKey' + # Check GPO policy override warnings + $gpoWarning = Get-GpoOverrideWarning -KeyPath $Operation.KeyPath -ValueName $Operation.ValueName + if ($gpoWarning) { + Write-Warning $gpoWarning + } + + if ($DryRun) { + switch ($operationType) { + 'DeleteKey' { + Write-Host "[WhatIf] Remove Registry Key (Tree): $($Operation.KeyPath)" -ForegroundColor Cyan + } + 'DeleteValue' { + Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $null -DryRun:$true + } + 'SetValue' { + Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $null -DryRun:$true + } + default { + throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'" + } + } + return + } + $keyInfo = Get-RegistryKeyForOperation -RegistryPath $Operation.KeyPath -CreateIfMissing:$isSetValueOperation -OpenKey:(-not $isDeleteKeyOperation) switch ($operationType) { @@ -196,7 +238,8 @@ function Invoke-RegistryOperation { function Invoke-RegistryOperationsFromRegFile { param( [Parameter(Mandatory)] - [string]$RegFilePath + [string]$RegFilePath, + [switch]$DryRun ) $accessDeniedCount = 0 @@ -205,7 +248,7 @@ function Invoke-RegistryOperationsFromRegFile { foreach ($operation in $operations) { try { - Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath + Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath -DryRun:$DryRun } catch [System.UnauthorizedAccessException], [System.Security.SecurityException] { $accessDeniedCount++ @@ -213,11 +256,13 @@ function Invoke-RegistryOperationsFromRegFile { } } - if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) { - throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions." - } + if (-not $DryRun) { + if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) { + throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions." + } - if ($accessDeniedCount -gt 0) { - Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'." + if ($accessDeniedCount -gt 0) { + Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'." + } } } diff --git a/Scripts/Helpers/RegistryPathHelpers.ps1 b/Scripts/Helpers/RegistryPathHelpers.ps1 index 683fd49a..df27a5bf 100644 --- a/Scripts/Helpers/RegistryPathHelpers.ps1 +++ b/Scripts/Helpers/RegistryPathHelpers.ps1 @@ -81,3 +81,59 @@ function Get-RegistryFilePathForFeature { return Join-Path $script:RegfilesPath $RegistryKey } + +function Get-GpoOverrideWarning { + param( + [string]$KeyPath, + [string]$ValueName + ) + + if ([string]::IsNullOrWhiteSpace($ValueName)) { + return $null + } + + # Common GPO policy registry roots in PowerShell provider syntax + $gpoPaths = @( + "Registry::HKEY_LOCAL_MACHINE\Software\Policies", + "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies", + "Registry::HKEY_CURRENT_USER\Software\Policies", + "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies" + ) + + $parts = Split-RegistryPath -path $KeyPath + if (-not $parts) { + return $null + } + + $subKey = $parts.SubKey + if ([string]::IsNullOrWhiteSpace($subKey)) { + return $null + } + + # Normalize subkey path by stripping leading Software\ + $cleanSubKey = $subKey -replace '^Software\\', '' + + foreach ($gpoRoot in $gpoPaths) { + # Build candidate paths mapping common structures + $candidatePaths = @( + "$gpoRoot\$cleanSubKey", + "$gpoRoot\" + ($cleanSubKey -replace '^Microsoft\\Windows\\CurrentVersion\\', ''), + "$gpoRoot\" + ($cleanSubKey -replace '^Microsoft\\', '') + ) + + foreach ($p in $candidatePaths) { + if (Test-Path -LiteralPath $p) { + try { + $val = Get-ItemPropertyValue -LiteralPath $p -Name $ValueName -ErrorAction SilentlyContinue + if ($null -ne $val) { + return "Group Policy (GPO) override detected at '$p' for value '$ValueName' (Value: $val). This policy may override your debloat changes." + } + } + catch {} + } + } + } + + return $null +} + From c6065140be7b2f60f04d5bfbb3ad9612fc853a0c Mon Sep 17 00:00:00 2001 From: HetCreep Date: Thu, 11 Jun 2026 20:22:55 +0700 Subject: [PATCH 2/4] feat(whatif): support dry-run previews for Appx removal, Store suggestions, and Start Menu replacement --- Scripts/AppRemoval/RemoveApps.ps1 | 7 +++++++ Scripts/Features/DisableStoreSearchSuggestions.ps1 | 10 ++++++++++ Scripts/Features/ReplaceStartMenu.ps1 | 10 ++++++++++ 3 files changed, 27 insertions(+) diff --git a/Scripts/AppRemoval/RemoveApps.ps1 b/Scripts/AppRemoval/RemoveApps.ps1 index 0695029d..8577b87c 100644 --- a/Scripts/AppRemoval/RemoveApps.ps1 +++ b/Scripts/AppRemoval/RemoveApps.ps1 @@ -4,6 +4,13 @@ function RemoveApps { $appslist ) + if ($WhatIfPreference) { + foreach ($app in $appslist) { + Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan + } + return + } + # Determine target from script-level params, defaulting to AllUsers $targetUser = GetTargetUserForAppRemoval diff --git a/Scripts/Features/DisableStoreSearchSuggestions.ps1 b/Scripts/Features/DisableStoreSearchSuggestions.ps1 index adb1780d..4d615678 100644 --- a/Scripts/Features/DisableStoreSearchSuggestions.ps1 +++ b/Scripts/Features/DisableStoreSearchSuggestions.ps1 @@ -28,6 +28,11 @@ function DisableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value + if ($WhatIfPreference) { + Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to store.db" -ForegroundColor Cyan + return + } + # This file doesn't exist in EEA (No Store app suggestions). if (-not (Test-Path -Path $StoreAppsDatabase)) { @@ -79,6 +84,11 @@ function EnableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value if (-not $userName) { $userName = '' } + if ($WhatIfPreference) { + Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to store.db" -ForegroundColor Cyan + return + } + if (-not (Test-Path -Path $StoreAppsDatabase)) { Write-Host "Store app database not found for user $userName, nothing to undo" return diff --git a/Scripts/Features/ReplaceStartMenu.ps1 b/Scripts/Features/ReplaceStartMenu.ps1 index 94f3e88a..55562b2b 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 ($WhatIfPreference) { + 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 ($WhatIfPreference) { + Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan + return + } + $backupBinFile = $startMenuBinFile + ".bak" if (Test-Path $startMenuBinFile) { From 80d8f1de83acf559d8156cd5bad31b0f4acd0c20 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Fri, 19 Jun 2026 02:57:53 +0700 Subject: [PATCH 3/4] refactor(whatif): simplify GPO check and expand dry-run guards --- Scripts/AppRemoval/RemoveApps.ps1 | 3 +- .../DisableStoreSearchSuggestions.ps1 | 6 +- Scripts/Features/ExecuteChanges.ps1 | 45 ++++++++++----- Scripts/Features/ImportRegistryFile.ps1 | 5 +- Scripts/Features/ReplaceStartMenu.ps1 | 16 +++++- Scripts/Features/RestartExplorer.ps1 | 6 ++ Scripts/Features/RestoreRegistryBackup.ps1 | 6 ++ Scripts/FileIO/SaveCustomAppsListToFile.ps1 | 6 ++ Scripts/FileIO/SaveSettings.ps1 | 6 ++ Scripts/GUI/Show-ConfigWindow.ps1 | 7 +++ Scripts/Helpers/ApplyRegistryRegFile.ps1 | 36 +++++------- Scripts/Helpers/RegistryPathHelpers.ps1 | 55 ------------------- Win11Debloat.ps1 | 9 +++ 13 files changed, 108 insertions(+), 98 deletions(-) diff --git a/Scripts/AppRemoval/RemoveApps.ps1 b/Scripts/AppRemoval/RemoveApps.ps1 index 8577b87c..96855dc4 100644 --- a/Scripts/AppRemoval/RemoveApps.ps1 +++ b/Scripts/AppRemoval/RemoveApps.ps1 @@ -4,7 +4,8 @@ function RemoveApps { $appslist ) - if ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { foreach ($app in $appslist) { Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan } diff --git a/Scripts/Features/DisableStoreSearchSuggestions.ps1 b/Scripts/Features/DisableStoreSearchSuggestions.ps1 index 4d615678..fbe35fb0 100644 --- a/Scripts/Features/DisableStoreSearchSuggestions.ps1 +++ b/Scripts/Features/DisableStoreSearchSuggestions.ps1 @@ -28,7 +28,8 @@ function DisableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value - if ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to store.db" -ForegroundColor Cyan return } @@ -84,7 +85,8 @@ function EnableStoreSearchSuggestions { $userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value if (-not $userName) { $userName = '' } - if ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to store.db" -ForegroundColor Cyan return } diff --git a/Scripts/Features/ExecuteChanges.ps1 b/Scripts/Features/ExecuteChanges.ps1 index 828cc419..76a4916d 100644 --- a/Scripts/Features/ExecuteChanges.ps1 +++ b/Scripts/Features/ExecuteChanges.ps1 @@ -73,7 +73,10 @@ 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 + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if (-not $isWhatIf) { + Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process + } RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime') } @@ -172,24 +175,30 @@ function ExecuteAllChanges { if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ } $currentStep = 0 + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") if ($hasRegistryBackedFeature) { $currentStep++ if ($script:ApplyProgressCallback) { & $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 ($isWhatIf) { + 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)" + } } } @@ -199,9 +208,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 ($isWhatIf) { + 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 de083321..0a94cce9 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -21,9 +21,10 @@ function ImportRegistryFile { $importScript = { param($targetRegFilePath, $hiveContext) - if ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Previewing registry changes for $path:" -ForegroundColor Cyan - Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath -DryRun:$true + Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath Write-Host "" return } diff --git a/Scripts/Features/ReplaceStartMenu.ps1 b/Scripts/Features/ReplaceStartMenu.ps1 index 55562b2b..86e66d79 100644 --- a/Scripts/Features/ReplaceStartMenu.ps1 +++ b/Scripts/Features/ReplaceStartMenu.ps1 @@ -26,7 +26,8 @@ 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 ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan return } @@ -66,7 +67,8 @@ function ReplaceStartMenu { $userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile - if ($WhatIfPreference) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan return } @@ -131,6 +133,16 @@ function RestoreStartMenuFromBackup { } $currentBinBackup = $StartMenuBinFile + '.restore.bak' + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + 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 diff --git a/Scripts/Features/RestartExplorer.ps1 b/Scripts/Features/RestartExplorer.ps1 index eff2bc21..28ba286e 100644 --- a/Scripts/Features/RestartExplorer.ps1 +++ b/Scripts/Features/RestartExplorer.ps1 @@ -5,6 +5,12 @@ function RestartExplorer { return } + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + 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 002fbb03..1c42170d 100644 --- a/Scripts/Features/RestoreRegistryBackup.ps1 +++ b/Scripts/Features/RestoreRegistryBackup.ps1 @@ -133,6 +133,12 @@ function Restore-RegistryBackupState { $friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target) + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan + return + } + $restoreAction = { param($normalizedBackup) diff --git a/Scripts/FileIO/SaveCustomAppsListToFile.ps1 b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 index 7ac8bdf5..e78365df 100644 --- a/Scripts/FileIO/SaveCustomAppsListToFile.ps1 +++ b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 @@ -4,6 +4,12 @@ function SaveCustomAppsListToFile { $appsList ) + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + 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 730808b1..13550fea 100644 --- a/Scripts/FileIO/SaveSettings.ps1 +++ b/Scripts/FileIO/SaveSettings.ps1 @@ -1,5 +1,11 @@ # Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file function SaveSettings { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + 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 14823697..1d798182 100644 --- a/Scripts/GUI/Show-ConfigWindow.ps1 +++ b/Scripts/GUI/Show-ConfigWindow.ps1 @@ -413,6 +413,13 @@ function Export-Configuration { Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))" + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan + Show-MessageBox -Message "[WhatIf] Configuration exported successfully." -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/Helpers/ApplyRegistryRegFile.ps1 b/Scripts/Helpers/ApplyRegistryRegFile.ps1 index 785d96ef..3cdaf156 100644 --- a/Scripts/Helpers/ApplyRegistryRegFile.ps1 +++ b/Scripts/Helpers/ApplyRegistryRegFile.ps1 @@ -100,14 +100,14 @@ function Invoke-RegistryDeleteValueOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - $KeyInfo, - [switch]$DryRun + $KeyInfo ) $valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName $displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName } - if ($DryRun) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { Write-Host "[WhatIf] Remove Registry Value: $($Operation.KeyPath) \ $displayValueName" -ForegroundColor Cyan return } @@ -130,12 +130,12 @@ function Invoke-RegistrySetValueOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - $KeyInfo, - [switch]$DryRun + $KeyInfo ) $setArgs = Convert-RegOperationToValueKind -Operation $Operation - if ($DryRun) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { $displayVal = if ($setArgs.Kind -eq [Microsoft.Win32.RegistryValueKind]::Binary) { "Binary data ($($setArgs.Value.Length) bytes)" } else { @@ -183,30 +183,24 @@ function Invoke-RegistryOperation { [Parameter(Mandatory)] $Operation, [Parameter(Mandatory)] - [string]$RegFilePath, - [switch]$DryRun + [string]$RegFilePath ) $operationType = [string]$Operation.OperationType $isSetValueOperation = $operationType -eq 'SetValue' $isDeleteKeyOperation = $operationType -eq 'DeleteKey' - # Check GPO policy override warnings - $gpoWarning = Get-GpoOverrideWarning -KeyPath $Operation.KeyPath -ValueName $Operation.ValueName - if ($gpoWarning) { - Write-Warning $gpoWarning - } - - if ($DryRun) { + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { switch ($operationType) { 'DeleteKey' { Write-Host "[WhatIf] Remove Registry Key (Tree): $($Operation.KeyPath)" -ForegroundColor Cyan } 'DeleteValue' { - Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $null -DryRun:$true + Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $null } 'SetValue' { - Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $null -DryRun:$true + Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $null } default { throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'" @@ -238,17 +232,17 @@ function Invoke-RegistryOperation { function Invoke-RegistryOperationsFromRegFile { param( [Parameter(Mandatory)] - [string]$RegFilePath, - [switch]$DryRun + [string]$RegFilePath ) $accessDeniedCount = 0 $operations = @(Get-RegFileOperations -regFilePath $RegFilePath) $totalOperations = $operations.Count + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") foreach ($operation in $operations) { try { - Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath -DryRun:$DryRun + Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath } catch [System.UnauthorizedAccessException], [System.Security.SecurityException] { $accessDeniedCount++ @@ -256,7 +250,7 @@ function Invoke-RegistryOperationsFromRegFile { } } - if (-not $DryRun) { + if (-not $isWhatIf) { if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) { throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions." } diff --git a/Scripts/Helpers/RegistryPathHelpers.ps1 b/Scripts/Helpers/RegistryPathHelpers.ps1 index df27a5bf..d666646e 100644 --- a/Scripts/Helpers/RegistryPathHelpers.ps1 +++ b/Scripts/Helpers/RegistryPathHelpers.ps1 @@ -82,58 +82,3 @@ function Get-RegistryFilePathForFeature { return Join-Path $script:RegfilesPath $RegistryKey } -function Get-GpoOverrideWarning { - param( - [string]$KeyPath, - [string]$ValueName - ) - - if ([string]::IsNullOrWhiteSpace($ValueName)) { - return $null - } - - # Common GPO policy registry roots in PowerShell provider syntax - $gpoPaths = @( - "Registry::HKEY_LOCAL_MACHINE\Software\Policies", - "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies", - "Registry::HKEY_CURRENT_USER\Software\Policies", - "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies" - ) - - $parts = Split-RegistryPath -path $KeyPath - if (-not $parts) { - return $null - } - - $subKey = $parts.SubKey - if ([string]::IsNullOrWhiteSpace($subKey)) { - return $null - } - - # Normalize subkey path by stripping leading Software\ - $cleanSubKey = $subKey -replace '^Software\\', '' - - foreach ($gpoRoot in $gpoPaths) { - # Build candidate paths mapping common structures - $candidatePaths = @( - "$gpoRoot\$cleanSubKey", - "$gpoRoot\" + ($cleanSubKey -replace '^Microsoft\\Windows\\CurrentVersion\\', ''), - "$gpoRoot\" + ($cleanSubKey -replace '^Microsoft\\', '') - ) - - foreach ($p in $candidatePaths) { - if (Test-Path -LiteralPath $p) { - try { - $val = Get-ItemPropertyValue -LiteralPath $p -Name $ValueName -ErrorAction SilentlyContinue - if ($null -ne $val) { - return "Group Policy (GPO) override detected at '$p' for value '$ValueName' (Value: $val). This policy may override your debloat changes." - } - } - catch {} - } - } - } - - return $null -} - diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index 5c627cf5..0da71ca1 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -220,6 +220,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 { + $isDomainJoined = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).PartOfDomain + if ($isDomainJoined) { + 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" From 4038148b0fa4844eae867fd1dc9dae21ced1910e Mon Sep 17 00:00:00 2001 From: HetCreep Date: Fri, 19 Jun 2026 23:47:46 +0700 Subject: [PATCH 4/4] fix(whatif): resolve GUI return types and clarify WhatIf messages --- Scripts/Features/RestoreRegistryBackup.ps1 | 5 +++-- Scripts/GUI/Show-ConfigWindow.ps1 | 2 +- Scripts/GUI/Show-RestoreBackupWindow.ps1 | 14 +++++++++++--- Win11Debloat.ps1 | 8 +++++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Scripts/Features/RestoreRegistryBackup.ps1 b/Scripts/Features/RestoreRegistryBackup.ps1 index 1c42170d..01a5b65a 100644 --- a/Scripts/Features/RestoreRegistryBackup.ps1 +++ b/Scripts/Features/RestoreRegistryBackup.ps1 @@ -136,7 +136,7 @@ function Restore-RegistryBackupState { $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") if ($isWhatIf) { Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan - return + return [PSCustomObject]@{ Result = $true } } $restoreAction = { @@ -154,9 +154,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/GUI/Show-ConfigWindow.ps1 b/Scripts/GUI/Show-ConfigWindow.ps1 index 1d798182..1a6c93c8 100644 --- a/Scripts/GUI/Show-ConfigWindow.ps1 +++ b/Scripts/GUI/Show-ConfigWindow.ps1 @@ -416,7 +416,7 @@ function Export-Configuration { $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") if ($isWhatIf) { Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan - Show-MessageBox -Message "[WhatIf] Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null + Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null return } diff --git a/Scripts/GUI/Show-RestoreBackupWindow.ps1 b/Scripts/GUI/Show-RestoreBackupWindow.ps1 index dc9fadd9..6fbfdf2f 100644 --- a/Scripts/GUI/Show-RestoreBackupWindow.ps1 +++ b/Scripts/GUI/Show-RestoreBackupWindow.ps1 @@ -27,9 +27,17 @@ 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 + $isWhatIf = $null -ne $script:Params -and $script:Params.ContainsKey("WhatIf") + if ($isWhatIf) { + $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 diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index 0da71ca1..cc0ec8cb 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -222,12 +222,14 @@ else { # Check if the device is domain-joined and warn the user (Group Policy may override changes) try { - $isDomainJoined = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).PartOfDomain - if ($isDomainJoined) { + $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 {} +catch { + # Suppress WMI/CIM query and null reference errors to ensure the script does not crash on startup +} # 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))) {