From 8b7a074092f192e3e7f16c6094155f880bd1d306 Mon Sep 17 00:00:00 2001 From: Shugoki <35711149+ShugokiFable@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:46:32 -0300 Subject: [PATCH 1/2] Audit toolkit correctness, recovery, DNS, repair and launchers --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 13 +- CONTRIBUTING.md | 2 +- Modules/Network.State.psm1 | 13 +- Modules/Toolkit.Core.psm1 | 123 ++++++- README.md | 24 +- START_TOOLKIT.bat | 6 +- Toolkit.Run.ps1 | 4 +- Toolkit.Worker.ps1 | 36 +- Toolkit.ps1 | 24 +- Toolkit.xaml | 2 +- VALIDATION_REPORT.md | 10 +- Validate_All.ps1 | 4 +- dns-encrypted-doh/DNS_Encrypted_Manager.bat | 41 +-- dns-encrypted-doh/DNS_Manager.ps1 | 96 +++--- dns-encrypted-doh/DNS_Restore_Previous.bat | 39 +-- dns-encrypted-doh/DNS_Revert_DHCP.bat | 39 +-- dns-encrypted-doh/DNS_Set_Mullvad_AdBlock.bat | 40 +-- dns-encrypted-doh/DNS_Set_Quad9.bat | 39 +-- dns-encrypted-doh/DNS_Set_Quad9_Mullvad.bat | 39 +-- dns-encrypted-doh/README.md | 9 +- docs/AUDIT_2026-10-10.md | 38 +++ docs/audit-cleaner.md | 30 ++ docs/audit-dns-privacy.md | 50 +++ docs/audit-repair-gaming.md | 38 +++ docs/images/dashboard.png | Bin 115431 -> 115248 bytes docs/specs/2026-09-18-pc-cleaner-design.md | 2 + pc-cleaner/PC_Cleaner.ps1 | 179 ++++++---- pc-cleaner/README.md | 17 +- pc-cleaner/Run_As_Admin.bat | 33 +- pc-corruption-fixer/Fix_Corruption.bat | 34 +- pc-corruption-fixer/PC_Fixer.ps1 | 316 ++++++++++-------- pc-corruption-fixer/README.md | 8 +- pc-gaming-optimizer/Fix_VR_PCVR.ps1 | 5 + .../Modules/Optimizer.Core.psm1 | 215 ++++++++---- pc-gaming-optimizer/PC_Optimizer.ps1 | 8 +- pc-gaming-optimizer/README.md | 8 +- pc-gaming-optimizer/Run_As_Admin.bat | 38 +-- pc-gaming-optimizer/Run_VR_Fix.bat | 38 +-- pc-privacy-guard/PC_Privacy.ps1 | 82 ++++- pc-privacy-guard/README.md | 8 +- pc-privacy-guard/Run_As_Admin.bat | 41 +-- tests/Test_All.ps1 | 6 + tests/Test_AuditCleaner.ps1 | 177 ++++++++++ tests/Test_AuditDnsPrivacy.ps1 | 177 ++++++++++ tests/Test_AuditLaunchers.ps1 | 72 ++++ tests/Test_AuditRepairGaming.ps1 | 234 +++++++++++++ tests/Test_AuditSuite.ps1 | 119 +++++++ tests/Test_Toolkit.ps1 | 20 +- 49 files changed, 1947 insertions(+), 653 deletions(-) create mode 100644 docs/AUDIT_2026-10-10.md create mode 100644 docs/audit-cleaner.md create mode 100644 docs/audit-dns-privacy.md create mode 100644 docs/audit-repair-gaming.md create mode 100644 tests/Test_All.ps1 create mode 100644 tests/Test_AuditCleaner.ps1 create mode 100644 tests/Test_AuditDnsPrivacy.ps1 create mode 100644 tests/Test_AuditLaunchers.ps1 create mode 100644 tests/Test_AuditRepairGaming.ps1 create mode 100644 tests/Test_AuditSuite.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ded10..53e280d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: - name: Real registry, filesystem, worker and WPF regression checks shell: powershell run: | - & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_Toolkit.ps1 + & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_All.ps1 if ($LASTEXITCODE -ne 0) { throw "Toolkit regression checks failed." } - name: Regression checks in the installed friendly folder layout @@ -79,7 +79,7 @@ jobs: foreach ($file in @('Toolkit.ps1','Toolkit.Worker.ps1','Toolkit.Run.ps1','Toolkit.xaml')) { Copy-Item -LiteralPath (Join-Path $PWD $file) -Destination $fixture } - & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File (Join-Path $fixture 'tests\Test_Toolkit.ps1') + & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File (Join-Path $fixture 'tests\Test_All.ps1') if ($LASTEXITCODE -ne 0) { throw "Friendly-layout regression checks failed." } data: diff --git a/CHANGELOG.md b/CHANGELOG.md index fda572c..ed36236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Toolkit 3.0.1 (2026-10-10, source audit) + +- Audited all components, wrappers, shared modules, dashboard/worker, recovery, tests, launchers, CI and documentation before further feature expansion. Detailed findings: [audit](docs/AUDIT_2026-10-10.md). +- Cleaner 1.1.1: make Windows.old truly report-only; prevent cleanup through ancestor junctions; align Temp scans with aged-file deletion; preserve replacements during registry/legacy recovery; report failed cache commands and retain unique logs. +- DNS 2.2.1: correct Quad9 secondary IPv6 to `2620:fe::9`; require typed DoH APIs; preflight address families and restore modes; verify writes, removals and DHCP reset with rollback on failure. +- Privacy 2.1.1: save the identity required by undo; verify snapshot inventories and writes; preserve typed arrays and delayed service startup; surface denied writes and require confirmation for legacy identity-less backups. +- Fixer 7.2.1 and Gaming 5.0.1: repair summary exit crash; retain native command verdicts; stop false-success repair/undo paths; narrow Store restore to the current user; scope and validate recovery snapshots; protect shader paths and preserve NTP configuration. +- Dashboard/core: preview reviewed selections; accept reviewed-only runs; lock profile controls while busy; retain worker startup/partial failure results; compare registry strings exactly; reject empty undo and linked state paths; use literal scheduled-task objects; verify stored AppX file hashes before reinstall. +- Launchers: replace service-dependent elevation checks, quoted-path interpolation and timed UAC flags with principal checks and waiting on the child process; propagate exit codes. +- Add native failure-path suites to CI in both folder layouts. Add bounded optional debug diagnostics under `%LOCALAPPDATA%\WindowsPCToolkit\Logs`. No new optimizer/debloat features or dependencies. + ## Toolkit 3.0.0 (2026-10-01) - New native WPF dashboard: Optimize, Startup/tasks, App debloat, Encrypted DNS, Recovery/reports and Repair/tools. One-click ten-action recommended profile, nine optional choices and reviewed app/startup selections in one journal. @@ -127,7 +138,7 @@ Major safety rewrite of every tool. First release with the modular Gaming Optimi ## Encrypted DNS Manager 2.1 -- Corrected Quad9 secondary IPv6 to the official secure-profile address `2620:fe::fe:9`; the previous value belonged to a different Quad9 service family. +- Historical change introduced the incorrect Quad9 secondary IPv6 `2620:fe::fe:9`. The official Secure address is `2620:fe::9`; corrected in 3.0.1. - Replaced the mixed Quad9/Mullvad adapter profile with one provider at a time. - Added official Quad9 and all currently published Mullvad profiles. - Every IPv4/IPv6 address is registered with its matching HTTPS DoH template. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc972cc..baec8d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Use built-in Windows PowerShell 5.1 on Windows, with STA for the WPF tests: ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Validate_All.ps1 -powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_Toolkit.ps1 +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_All.ps1 ``` Tests create disposable registry/filesystem fixtures and use simulated mutation commands for DNS, AppX, optional features and update repair. They exercise native WPF/jobs, stay-awake and read-only inventories; they do not optimize or repair the host. CI runs the same suite in both supported folder layouts. See [validation details](VALIDATION_REPORT.md). diff --git a/Modules/Network.State.psm1 b/Modules/Network.State.psm1 index 162a1d8..62c9e14 100644 --- a/Modules/Network.State.psm1 +++ b/Modules/Network.State.psm1 @@ -19,14 +19,19 @@ function Get-AdapterStaticDnsServers { function Set-AdapterDnsFamilies { param([int]$Index,[AllowEmptyCollection()][string[]]$IPv4=@(),[AllowEmptyCollection()][string[]]$IPv6=@()) - foreach ($family in @(@{Id=2;Servers=$IPv4},@{Id=23;Servers=$IPv6})) { - $inputObject=Get-DnsClientServerAddress -InterfaceIndex $Index -ErrorAction Stop | Where-Object { [int]$_.AddressFamily -eq $family.Id } + $addresses=@(Get-DnsClientServerAddress -InterfaceIndex $Index -ErrorAction Stop) + if (-not $addresses.Count) { throw "Adapter $Index exposes no DNS address families." } + $changes=foreach ($family in @(@{Id=2;Servers=$IPv4},@{Id=23;Servers=$IPv6})) { + $inputObject=$addresses | Where-Object { [int]$_.AddressFamily -eq $family.Id } if (-not $inputObject) { if (@($family.Servers).Count) { throw "Adapter $Index does not expose address family $($family.Id)." } continue } - if (@($family.Servers).Count) { Set-DnsClientServerAddress -InputObject $inputObject -ServerAddresses $family.Servers -ErrorAction Stop } - else { Set-DnsClientServerAddress -InputObject $inputObject -ResetServerAddresses -ErrorAction Stop } + @{InputObject=$inputObject;Servers=$family.Servers} + } + foreach ($change in $changes) { + if (@($change.Servers).Count) { Set-DnsClientServerAddress -InputObject $change.InputObject -ServerAddresses $change.Servers -ErrorAction Stop } + else { Set-DnsClientServerAddress -InputObject $change.InputObject -ResetServerAddresses -ErrorAction Stop } } } Export-ModuleMember -Function Set-AdapterDnsFamilies,Get-AdapterStaticDnsServers diff --git a/Modules/Toolkit.Core.psm1 b/Modules/Toolkit.Core.psm1 index 71ac67a..75ec58b 100644 --- a/Modules/Toolkit.Core.psm1 +++ b/Modules/Toolkit.Core.psm1 @@ -4,7 +4,32 @@ Set-StrictMode -Version 2.0 $ErrorActionPreference='Stop' $script:SuiteRoot=Split-Path $PSScriptRoot -Parent $script:SuiteDataRoot=$DataRoot -$script:SuiteVersion='3.0.0' +$script:SuiteVersion='3.0.1' + +function Write-SuiteDiagnostic { + param([string]$Operation,[string]$Message,[string]$Level='Debug',[string]$RunId) + if ($Level -eq 'Debug' -and $env:WINDOWS_PC_TOOLKIT_DEBUG -ne '1') { return } + try { + $directory=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs' + [void][IO.Directory]::CreateDirectory($directory) + $path=Join-Path $directory 'Suite.log' + if ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).Length -gt 5MB) { Move-Item -LiteralPath $path -Destination ($path+'.previous') -Force } + $line=[ordered]@{Time=(Get-Date).ToString('o');Version=$script:SuiteVersion;Level=$Level;Operation=$Operation;RunId=$RunId;Message=$Message.Substring(0,[Math]::Min($Message.Length,4096))} + [IO.File]::AppendAllText($path,($line | ConvertTo-Json -Compress)+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false))) + } catch { Write-Verbose "Diagnostic log unavailable: $($_.Exception.Message)" } +} +function Assert-SuiteUnlinkedPath { + param([string]$Path) + $current=[IO.Path]::GetFullPath($Path) + while ($current) { + if (Test-Path -LiteralPath $current) { + if ((Get-Item -LiteralPath $current -Force -ErrorAction Stop).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Toolkit state/package path contains a link: $current" } + } + $parent=Split-Path $current -Parent + if ($parent -eq $current) { break } + $current=$parent + } +} function Resolve-SuitePath { param([string[]]$Candidates) @@ -21,6 +46,7 @@ function Get-SuiteDataRoot { $script:SuiteDataRoot } function Get-SuiteVersion { $script:SuiteVersion } function Get-SuiteIdentity { [Security.Principal.WindowsIdentity]::GetCurrent().User.Value } function Initialize-SuiteState { + Assert-SuiteUnlinkedPath $script:SuiteDataRoot if (-not (Test-Path -LiteralPath $script:SuiteDataRoot)) { [void][IO.Directory]::CreateDirectory($script:SuiteDataRoot) $acl=New-Object Security.AccessControl.DirectorySecurity @@ -32,10 +58,15 @@ function Initialize-SuiteState { } Set-Acl -LiteralPath $script:SuiteDataRoot -AclObject $acl } - foreach ($name in @('Runs','Requests','AppBackups')) { [void][IO.Directory]::CreateDirectory((Join-Path $script:SuiteDataRoot $name)) } + foreach ($name in @('Runs','Requests','AppBackups')) { + $path=Join-Path $script:SuiteDataRoot $name + Assert-SuiteUnlinkedPath $path + [void][IO.Directory]::CreateDirectory($path) + } } function Save-SuiteJson { param([string]$Path,[object]$Value) + Assert-SuiteUnlinkedPath $Path $json=$Value | ConvertTo-Json -Depth 16 $null=$json | ConvertFrom-Json $temporary=$Path+'.'+[guid]::NewGuid().ToString('N')+'.tmp' @@ -67,7 +98,7 @@ function Test-SuiteRegistryEqual { param($First,$Second) if ([bool]$First.Exists -ne [bool]$Second.Exists) { return $false } if (-not $First.Exists) { return $true } - return ($First.Kind -eq $Second.Kind -and (ConvertTo-Json -InputObject $First.Value -Compress) -eq (ConvertTo-Json -InputObject $Second.Value -Compress)) + return ($First.Kind -eq $Second.Kind -and (ConvertTo-Json -InputObject $First.Value -Compress) -ceq (ConvertTo-Json -InputObject $Second.Value -Compress)) } function Set-SuiteRegistry { param($Run,[string]$Path,[string]$Name,$Value,[string]$Kind='DWord',[switch]$Remove) @@ -185,6 +216,7 @@ function Test-SuiteMissingExecutable { param([string]$Path) if (-not $Path -or $Path -notmatch '^[a-zA-Z]:\\' -or [IO.Path]::GetExtension($Path) -ne '.exe') { return $false } try { + Assert-SuiteUnlinkedPath $Path $drive=New-Object IO.DriveInfo([IO.Path]::GetPathRoot($Path)) if (-not $drive.IsReady -or $drive.DriveType -ne 'Fixed') { return $false } [void][IO.File]::GetAttributes($Path) @@ -219,6 +251,12 @@ function Get-SuiteAppCatalog { [pscustomobject]@{Id=$app.PackageFullName;Name=$app.Name;Version=[string]$app.Version;Location=$app.InstallLocation;Detail='Optional consumer app. Removal affects this user and its app data; package reinstall is available.'} } } +function Get-SuiteTask { + param([string]$Path,[string]$Name) + $tasks=@(Get-ScheduledTask -TaskPath $Path -TaskName $Name -ErrorAction Stop | Where-Object { $_.TaskPath -eq $Path -and $_.TaskName -eq $Name }) + if ($tasks.Count -ne 1) { throw 'The selected scheduled task changed or is missing. Refresh the list.' } + return $tasks[0] +} function Invoke-SuiteNative { param([string]$File,[string[]]$Arguments,[int[]]$SuccessCodes=@(0),[Text.Encoding]$OutputEncoding) $psi=New-Object Diagnostics.ProcessStartInfo @@ -245,7 +283,7 @@ function Get-SuiteHealth { [pscustomobject]@{OS=$os.Caption;Build=$os.BuildNumber;CPU=$cpu.Name;RAMGB=[math]::Round($os.TotalVisibleMemorySize/1MB,1);FreeRAMGB=[math]::Round($os.FreePhysicalMemory/1MB,1);GPU=@(Get-CimInstance Win32_VideoController -ErrorAction Stop | Select-Object Name,DriverVersion);Services=$services;UpdatesDisabled=@($services | Where-Object { $_.Name -in @('wuauserv','BITS','UsoSvc','WaaSMedicSvc') -and $_.StartMode -eq 'Disabled' }).Count;PowerScheme=(Get-ActivePowerSchemeGuid)} } function Get-SuitePreview { - param([string[]]$ActionIds) + param([string[]]$ActionIds,[string[]]$StartupIds=@(),[string[]]$AppIds=@()) $catalog=@(Get-SuiteActionCatalog) if ($null -eq $ActionIds) { $ActionIds=@(($catalog | Where-Object Default).Id) } foreach ($id in $ActionIds) { @@ -258,12 +296,29 @@ function Get-SuitePreview { } [pscustomobject]@{Action=$action.Name;Category=$action.Category;Details=$details;Changes=@($action.Targets).Count} } + if ($StartupIds.Count) { + $items=@(Get-SuiteStartupItems) + foreach ($id in $StartupIds | Select-Object -Unique) { + $item=@($items | Where-Object Id -eq $id) + if ($item.Count -ne 1) { throw 'A selected startup entry changed. Refresh the list.' } + [pscustomobject]@{Action='Disable startup: '+$item[0].Name;Category='Reviewed startup';Details=$item[0].Target;Changes=1} + } + } + if ($AppIds.Count) { + $items=@(Get-SuiteAppCatalog) + foreach ($id in $AppIds | Select-Object -Unique) { + $item=@($items | Where-Object Id -eq $id) + if ($item.Count -ne 1) { throw 'A selected app changed or is protected. Refresh the list.' } + [pscustomobject]@{Action='Remove app: '+$item[0].Name;Category='Reviewed apps';Details='Verified package backup first; app data may be deleted.';Changes=1} + } + } } function Add-SuiteStep { param($Run,[string]$Action,[string]$Status,[string]$Message) [void]$Run.Steps.Add([pscustomobject]@{Action=$Action;Status=$Status;Message=$Message;Time=(Get-Date).ToString('o')}) $Run.Message=$Message Save-SuiteJson $Run.Path $Run + Write-SuiteDiagnostic -Operation $Action -Message $Message -RunId $Run.Id -Level $(if ($Status -eq 'Failed') { 'Error' } else { 'Debug' }) } function Invoke-SuiteRun { param([string[]]$ActionIds,[string[]]$StartupIds=@(),[string[]]$AppIds=@(),[scriptblock]$Progress) @@ -319,7 +374,7 @@ function Invoke-SuiteRun { $result=Disable-WindowsOptionalFeature -Online -FeatureName Recall -NoRestart -ErrorAction Stop $actual=[string](Get-WindowsOptionalFeature -Online -FeatureName Recall -ErrorAction Stop).State if ($actual -notin @('Disabled','DisabledWithPayloadRemoved','DisablePending')) { throw 'Recall disable read-back failed.' } - $entry.After=$actual; $entry.Status='Applied'; Save-SuiteJson $run.Path $run + $entry.After=if ($actual -eq 'DisablePending') { 'Disabled' } else { $actual }; $entry.Status='Applied'; Save-SuiteJson $run.Path $run $message='Recall disabled through Windows servicing. Feature payload kept for undo; snapshot data is not restored.' if ($result.RestartNeeded -or $actual -eq 'DisablePending') { $message+=' Restart Windows to finish.' } } @@ -339,7 +394,10 @@ function Invoke-SuiteRun { } 'RegistryCare' { $count=0 - foreach ($item in Get-SuiteStartupItems | Where-Object { $_.Kind -eq 'Startup' -and $_.Missing }) { Set-SuiteRegistry $run $item.Path $item.ValueName $null -Remove; $count++ } + foreach ($item in Get-SuiteStartupItems | Where-Object { $_.Kind -eq 'Startup' -and $_.Missing }) { + $current=Get-RegistryValueState $item.Path $item.ValueName + if ($current.Exists -and (Test-SuiteMissingExecutable (Get-CommandTargetPath ([string]$current.Value)))) { Set-SuiteRegistry $run $item.Path $item.ValueName $null -Remove; $count++ } + } foreach ($issue in Get-RegistryIssues | Where-Object { $_.Category -eq 'Stale MuiCache entries' }) { if (Test-SuiteMissingExecutable $issue.Display) { Set-SuiteRegistry $run $issue.Key $issue.Name $null -Remove; $count++ } } @@ -384,11 +442,12 @@ function Invoke-SuiteStartupDisable { try { if ($item.Kind -eq 'Startup') { Set-SuiteRegistry $run $item.Path $item.ValueName $null -Remove } else { - $task=Get-ScheduledTask -TaskPath $item.Path -TaskName $item.ValueName -ErrorAction Stop + $task=Get-SuiteTask $item.Path $item.ValueName + if (-not $task.Settings.Enabled) { Add-SuiteStep $run $item.Name 'Skipped' 'Startup task was already disabled.'; continue } $entry=[pscustomobject]@{Path=$item.Path;Name=$item.ValueName;Enabled=[bool]$task.Settings.Enabled;Status='Pending'} [void]$run.Tasks.Add($entry); Save-SuiteJson $run.Path $run - Disable-ScheduledTask -TaskPath $item.Path -TaskName $item.ValueName -ErrorAction Stop | Out-Null - if ((Get-ScheduledTask -TaskPath $item.Path -TaskName $item.ValueName -ErrorAction Stop).Settings.Enabled) { throw 'Task disable verification failed.' } + Disable-ScheduledTask -InputObject $task -ErrorAction Stop | Out-Null + if ((Get-SuiteTask $item.Path $item.ValueName).Settings.Enabled) { throw 'Task disable verification failed.' } $entry.Status='Applied' } Add-SuiteStep $run $item.Name 'Applied' 'Startup item disabled; original state saved.' @@ -411,20 +470,23 @@ function Invoke-SuiteAppRemoval { foreach ($app in $available | Where-Object { $Ids -contains $_.Id }) { try { $source=Get-Item -LiteralPath $app.Location -ErrorAction Stop + Assert-SuiteUnlinkedPath $source.FullName if ($source.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Package folder is a link; kept the app.' } if (@(Get-ChildItem -LiteralPath $source.FullName -Recurse -Force -ErrorAction Stop | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }).Count) { throw 'Package contains linked files/folders; kept the app.' } $backup=Join-Path $script:SuiteDataRoot ('AppBackups\'+$run.Id+'\'+$app.Id) + Assert-SuiteUnlinkedPath $backup [void][IO.Directory]::CreateDirectory($backup) # robocopy preserves long package paths and skips junctions. Codes 0-7 are success. $null=Invoke-SuiteNative "$env:SystemRoot\System32\robocopy.exe" @($app.Location,$backup,'/E','/XJ','/R:0','/W:0','/NFL','/NDL','/NJH','/NJS') @(0,1,2,3,4,5,6,7) if (-not (Test-Path -LiteralPath (Join-Path $backup 'AppxManifest.xml'))) { throw 'Package backup is incomplete; kept the app.' } - foreach ($file in Get-ChildItem -LiteralPath $source.FullName -Recurse -File -Force -ErrorAction Stop) { + $hashes=@(foreach ($file in Get-ChildItem -LiteralPath $source.FullName -Recurse -File -Force -ErrorAction Stop) { if ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'A package file is a link; kept the app.' } $relative=$file.FullName.Substring($source.FullName.TrimEnd('\').Length).TrimStart('\') $copy=Join-Path $backup $relative if (-not (Test-Path -LiteralPath $copy) -or (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash) { throw 'Package backup verification failed; kept the app.' } - } - $entry=[pscustomobject]@{Name=$app.Name;Package=$app.Id;Backup=$backup;Status='Pending'} + [pscustomobject]@{RelativePath=$relative;Hash=(Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash} + }) + $entry=[pscustomobject]@{Name=$app.Name;Package=$app.Id;Backup=$backup;Files=$hashes;Status='Pending'} [void]$run.Apps.Add($entry); Save-SuiteJson $run.Path $run Remove-AppxPackage -Package $app.Id -ErrorAction Stop if (Get-AppxPackage -Name $app.Name -ErrorAction Stop) { throw 'Package is still installed.' } @@ -438,8 +500,13 @@ function Invoke-SuiteAppRemoval { function Get-SuiteHistory { $directory=Join-Path $script:SuiteDataRoot 'Runs' if (-not (Test-Path -LiteralPath $directory)) { return } + Assert-SuiteUnlinkedPath $directory foreach ($file in Get-ChildItem -LiteralPath $directory -Filter '*.json' -File | Sort-Object Name -Descending) { - try { $run=Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json; if ($run.UserSid -eq (Get-SuiteIdentity)) { $run } } catch { Write-Warning "Unreadable report: $($file.Name)" } + try { + Assert-SuiteUnlinkedPath $file.FullName + $run=Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json + if ($run.Schema -eq 1 -and $run.Computer -eq $env:COMPUTERNAME -and $run.UserSid -eq (Get-SuiteIdentity)) { $run.Path=$file.FullName; $run } + } catch { Write-Warning "Unreadable report: $($file.Name)" } } } function Invoke-SuiteUndo { @@ -448,9 +515,14 @@ function Invoke-SuiteUndo { $full=[IO.Path]::GetFullPath($Path) $root=[IO.Path]::GetFullPath((Join-Path $script:SuiteDataRoot 'Runs')).TrimEnd('\')+'\' if (-not $full.StartsWith($root,[StringComparison]::OrdinalIgnoreCase) -or [IO.Path]::GetExtension($full) -ne '.json') { throw 'Select a report from this toolkit history.' } + Assert-SuiteUnlinkedPath $full $run=Get-Content -LiteralPath $full -Raw -Encoding UTF8 | ConvertFrom-Json if ($run.Schema -ne 1 -or $run.Computer -ne $env:COMPUTERNAME -or $run.UserSid -ne (Get-SuiteIdentity)) { throw 'This backup belongs to another computer/user or uses an unsupported schema.' } if ($run.Status -eq 'Restored') { $run.Message='This operation was already restored. Current settings have been preserved.'; return $run } + if (-not (@($run.Registry).Count + @($run.Tasks).Count + @($run.Apps).Count) -and + -not ($run.PSObject.Properties.Name -contains 'Features' -and @($run.Features).Count) -and + -not ($run.PSObject.Properties.Name -contains 'DnsBackup' -and $run.DnsBackup)) { throw 'This report contains no reversible changes. Repair and cache/temp deletion cannot be undone.' } + $run.Path=$full $lock=Enter-SuiteOperation try { $errors=New-Object Collections.ArrayList @@ -467,9 +539,9 @@ function Invoke-SuiteUndo { foreach ($entry in @($run.Tasks)) { try { if ($entry.Status -eq 'Restored') { continue } - $task=Get-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name -ErrorAction Stop - if ($entry.Enabled -and -not $task.Settings.Enabled) { Enable-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name -ErrorAction Stop | Out-Null } - if ([bool](Get-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name -ErrorAction Stop).Settings.Enabled -ne [bool]$entry.Enabled) { throw 'Task was changed after optimization; current state preserved.' } + $task=Get-SuiteTask $entry.Path $entry.Name + if ($entry.Enabled -and -not $task.Settings.Enabled) { Enable-ScheduledTask -InputObject $task -ErrorAction Stop | Out-Null } + if ([bool](Get-SuiteTask $entry.Path $entry.Name).Settings.Enabled -ne [bool]$entry.Enabled) { throw 'Task was changed after optimization; current state preserved.' } $entry.Status='Restored' } catch { [void]$errors.Add($_.Exception.Message) } Save-SuiteJson $full $run @@ -478,6 +550,20 @@ function Invoke-SuiteUndo { try { if ($entry.Status -eq 'Restored') { continue } if (-not (Get-AppxPackage -Name $entry.Name -ErrorAction Stop)) { + $backup=[IO.Path]::GetFullPath($entry.Backup).TrimEnd('\')+'\' + $backupRoot=[IO.Path]::GetFullPath((Join-Path $script:SuiteDataRoot 'AppBackups')).TrimEnd('\')+'\' + if (-not $backup.StartsWith($backupRoot,[StringComparison]::OrdinalIgnoreCase)) { throw 'App backup is outside toolkit storage.' } + Assert-SuiteUnlinkedPath $backup + if (@(Get-ChildItem -LiteralPath $backup -Recurse -Force -ErrorAction Stop | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }).Count) { throw 'App backup contains links; use Microsoft Store to reinstall.' } + if ($entry.PSObject.Properties.Name -notcontains 'Files' -or -not @($entry.Files).Count) { throw 'Legacy app backup has no saved hashes. Reinstall this app using Microsoft Store.' } + $files=@(Get-ChildItem -LiteralPath $backup -Recurse -File -Force -ErrorAction Stop) + if ($files.Count -ne @($entry.Files).Count) { throw 'App backup contents changed; use Microsoft Store to reinstall.' } + foreach ($file in $entry.Files) { + $copy=[IO.Path]::GetFullPath((Join-Path $backup $file.RelativePath)) + if (-not $copy.StartsWith($backup,[StringComparison]::OrdinalIgnoreCase)) { throw 'Invalid app backup file path.' } + Assert-SuiteUnlinkedPath $copy + if ((Get-FileHash -LiteralPath $copy -Algorithm SHA256 -ErrorAction Stop).Hash -cne $file.Hash) { throw 'App backup hash changed; use Microsoft Store to reinstall.' } + } Add-AppxPackage -Register (Join-Path $entry.Backup 'AppxManifest.xml') -DisableDevelopmentMode -ErrorAction Stop } if (-not (Get-AppxPackage -Name $entry.Name -ErrorAction Stop)) { throw "Reinstall failed: $($entry.Name). Use Microsoft Store to reinstall." } @@ -492,7 +578,7 @@ function Invoke-SuiteUndo { $current=[string](Get-WindowsOptionalFeature -Online -FeatureName $entry.Name -ErrorAction Stop).State if ($current -eq $entry.Before) { $entry.Status='Restored'; continue } if ($current -in @('EnablePending','DisablePending')) { throw 'Restart Windows before restoring the Recall feature.' } - if ($current -ne $entry.After) { throw 'Windows feature changed since optimization; current state preserved.' } + if ($current -ne $entry.After -and -not ($entry.After -eq 'DisablePending' -and $current -eq 'Disabled')) { throw 'Windows feature changed since optimization; current state preserved.' } $result=Enable-WindowsOptionalFeature -Online -FeatureName $entry.Name -NoRestart -ErrorAction Stop $actual=[string](Get-WindowsOptionalFeature -Online -FeatureName $entry.Name -ErrorAction Stop).State if ($actual -eq 'EnablePending' -or $result.RestartNeeded) { throw 'Recall restore requested. Restart Windows, then retry undo to verify.' } @@ -519,13 +605,14 @@ function Invoke-SuiteRepair { $run=New-SuiteRun 'Repair' try { Enable-StayAwake } catch { Add-SuiteStep $run 'Stay awake' 'Warning' $_.Exception.Message } foreach ($step in @( - @{Name='DISM component-store repair';File='dism.exe';Args=@('/Online','/Cleanup-Image','/RestoreHealth')}, + @{Name='DISM component-store repair';File='dism.exe';Args=@('/Online','/Cleanup-Image','/RestoreHealth','/NoRestart')}, @{Name='SFC system-file scan';File='sfc.exe';Args=@('/scannow')})) { $run.Message='Running: '+$step.Name; Save-SuiteJson $run.Path $run if ($Progress) { & $Progress $run } try { $native=Invoke-SuiteNative (Join-Path "$env:SystemRoot\System32" $step.File) $step.Args @(0,3010) Add-SuiteStep $run $step.Name 'Completed' ($native.Output+$(if ($native.RestartRequired) { ' Restart required.' } else { '' })) + if ($native.RestartRequired) { Add-SuiteStep $run 'Remaining repair commands' 'Skipped' 'Restart Windows before continuing system repair.'; break } } catch { Add-SuiteStep $run $step.Name 'Failed' $_.Exception.Message; break } } $run.Status=if (@($run.Steps | Where-Object Status -eq 'Failed').Count) { 'CompletedWithErrors' } else { 'Completed' } diff --git a/README.md b/README.md index 1fa006d..47d393d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ **[Download v3.0.0](https://github.com/SenjuWoo/windows-pc-toolkit/releases/download/v3.0.0/Windows-PC-Toolkit-3.0.0.zip)** · [Release notes and checksums](https://github.com/SenjuWoo/windows-pc-toolkit/releases/tag/v3.0.0) +**Current source: 3.0.1 correctness audit.** This fixes cleanup, DNS, privacy recovery, repair reporting and launcher defects found in 3.0.0. See the [whole-project audit](docs/AUDIT_2026-10-10.md). The public download above is the older release; source fixes are not present in that ZIP. + ![Toolkit dashboard](docs/images/dashboard.png) Rendered from the actual WPF dashboard with initial selections. Hardware and results load on the target PC. @@ -31,6 +33,8 @@ For an upgrade, close the old dashboard and launch the newly extracted copy. Exi **Preview changes** explains the plan without applying it. Nine additional choices cover captures, mouse acceleration, animations, Explorer/taskbar preferences, Delivery Optimization cleanup and SSD ReTrim. Some settings need sign-out to refresh; no restart is forced. +Preview includes reviewed startup/app selections. Reviewed selections also work when every profile checkbox is off; controls stay locked while a saved operation runs. + The one-click profile names installed apps in its removal confirmation; package backups are verified before each uninstall. To keep consumer apps, untick **Remove Copilot and optional consumer apps**. For individual app removals and startup control, select rows on their pages and tick **Also apply my selected startup entries and app removals**. These changes share one recovery journal. ## Six organized pages @@ -58,6 +62,8 @@ Strict privacy, network reset and shader troubleshooting remain explicit advance Existing DoH entries are updated in place. Restore matches adapter GUIDs and separately preserves IPv4/IPv6 automatic versus static modes. Unrelated adapters, VPNs and other providers' DoH entries are preserved. Failed apply attempts restore the previous state and report any rollback failure. +DNS writes require Windows 11's typed DnsClient DoH cmdlets; unavailable APIs fail before changes. The Quad9 Secure IPv6 pair is `2620:fe::fe`, `2620:fe::9`, corrected against [Quad9's service table](https://docs.quad9.net/services/). + Quad9 requires its live TXT transport test to report `doh`; an unavailable test fails verification. AdGuard checks configuration/resolution without claiming the same live attestation. **Mullvad public encrypted DNS ends November 2, 2026.** Its new-apply choices are removed now; old backups remain restorable. Historical Mullvad launchers open the supported-provider menu. Use Quad9 or AdGuard for ongoing service. [Mullvad announcement](https://mullvad.net/en/blog/2026/9/3/shutting-down-our-public-encrypted-dns-servers-and-sponsoring-quad9-instead), [Quad9 services](https://docs.quad9.net/services/), [AdGuard DNS](https://adguard-dns.io/en/public-dns.html). @@ -74,6 +80,8 @@ Dashboard reports, typed registry journals, task states and verified package cop Registry state is saved **before** mutation, preserves types/literal environment strings, and is checked after restore. Undo keeps intervening registry changes and lists conflicts. Repeating completed undo preserves current state. Failed registry actions attempt to restore partial changes. +App package copies now retain per-file hashes and are rechecked before registration. Changed copies are refused. Older app backups without saved hashes require reinstall through Microsoft Store. Repair/cache-only reports explicitly say that they contain no reversible changes. + Recall is disabled using the supported Windows optional-feature API with no forced restart. Undo re-enables the recorded feature; pending servicing changes require a restart before verification. The dashboard removes the modern Copilot app rather than relying on the retiring legacy Copilot policy. AI controls preserve Windows AI frameworks and separate local-AI applications/model stores. Browser-policy support varies by version/profile. [Copilot removal](https://learn.microsoft.com/en-us/windows/client-management/manage-windows-copilot), [Recall management](https://learn.microsoft.com/en-us/windows/client-management/manage-recall), [Edge model policy](https://learn.microsoft.com/en-us/deployedge/microsoft-edge-browser-policies/genailocalfoundationalmodelsettings). Deleted temporary/cache files, Recall snapshots and browser-built-in AI models are not recoverable through the journal. Windows/Store updates may offer apps again; removal affects the current account, not provisioning for future accounts. App removal can delete app data; backups support package re-registration, not app-data recovery. Dependencies/servicing may prevent re-registration; the report identifies failures and Microsoft Store remains the reinstall route. System repair is logged maintenance, not a reversible registry tweak. @@ -84,12 +92,12 @@ Advanced consoles retain directories under `%ProgramData%\WindowsPCToolkit`: `Ga | Component | Version | Documentation | | --- | ---: | --- | -| Dashboard / core | 3.0.0 | This page | -| Gaming Optimizer | 5.0 | [Gaming](pc-gaming-optimizer/README.md) | -| PC Cleaner | 1.1 | [Cleanup/registry](pc-cleaner/README.md) | -| Corruption Fixer | 7.2 | [Repair](pc-corruption-fixer/README.md) | -| Privacy Guard | 2.1 | [Privacy](pc-privacy-guard/README.md) | -| Encrypted DNS | 2.2 | [DNS](dns-encrypted-doh/README.md) | +| Dashboard / core | 3.0.1 | This page | +| Gaming Optimizer | 5.0.1 | [Gaming](pc-gaming-optimizer/README.md) | +| PC Cleaner | 1.1.1 | [Cleanup/registry](pc-cleaner/README.md) | +| Corruption Fixer | 7.2.1 | [Repair](pc-corruption-fixer/README.md) | +| Privacy Guard | 2.1.1 | [Privacy](pc-privacy-guard/README.md) | +| Encrypted DNS | 2.2.1 | [DNS](dns-encrypted-doh/README.md) | ## Requirements and validation @@ -101,7 +109,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Toolkit.Run.ps1 -Previ # Static checks powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Validate_All.ps1 # Disposable registry/filesystem, actual WPF, simulated failures -powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_Toolkit.ps1 +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_All.ps1 ``` Tests do not apply live optimization or change live DNS/update services. [Validation evidence and limits](VALIDATION_REPORT.md). + +Detailed diagnostics are quiet by default. Launch `Toolkit.ps1 -DebugLog`, or set `WINDOWS_PC_TOOLKIT_DEBUG=1` for the dashboard and consoles. Bounded diagnostic logs are in `%LOCALAPPDATA%\WindowsPCToolkit\Logs` (`Suite.log` for the dashboard/worker); saved operation reports remain in ProgramData. Failures retain their causes even with debug off. diff --git a/START_TOOLKIT.bat b/START_TOOLKIT.bat index 7a5fa00..518d37d 100644 --- a/START_TOOLKIT.bat +++ b/START_TOOLKIT.bat @@ -1,10 +1,12 @@ @echo off -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\WindowsPCToolkit_launch.log" "%PS%" -NoProfile -STA -WindowStyle Hidden -ExecutionPolicy Bypass -File "%~dp0Toolkit.ps1" >"%LOG%" 2>&1 -if errorlevel 1 ( +set "RC=%ERRORLEVEL%" +if not "%RC%"=="0" ( echo Windows PC Toolkit could not start. Details: %LOG% type "%LOG%" pause ) +exit /b %RC% diff --git a/Toolkit.Run.ps1 b/Toolkit.Run.ps1 index 7f2932c..617e2f3 100644 --- a/Toolkit.Run.ps1 +++ b/Toolkit.Run.ps1 @@ -1,7 +1,9 @@ #requires -version 5.1 -param([string[]]$ActionIds,[switch]$Preview) +param([string[]]$ActionIds,[switch]$Preview,[switch]$DebugLog) $ErrorActionPreference='Stop' +if ($DebugLog) { $env:WINDOWS_PC_TOOLKIT_DEBUG='1' } Import-Module (Join-Path $PSScriptRoot 'Modules\Toolkit.Core.psm1') -Force -DisableNameChecking +Write-SuiteDiagnostic 'CLI startup' "DataRoot=$(Get-SuiteDataRoot); Preview=$Preview; Debug=$($env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1')" if ($Preview) { Get-SuitePreview -ActionIds $ActionIds | Format-Table Action,Category,Details -Wrap; exit 0 } if ($null -eq $ActionIds) { $ActionIds=@(Get-SuiteActionCatalog | Where-Object Default | ForEach-Object Id) } $apps=@() diff --git a/Toolkit.Worker.ps1 b/Toolkit.Worker.ps1 index d7bf473..ade4873 100644 --- a/Toolkit.Worker.ps1 +++ b/Toolkit.Worker.ps1 @@ -1,16 +1,19 @@ #requires -version 5.1 param([Parameter(Mandatory)][string]$RequestPath) $ErrorActionPreference='Stop' -Import-Module (Join-Path $PSScriptRoot 'Modules\Toolkit.Core.psm1') -Force -DisableNameChecking -$request=Get-Content -LiteralPath $RequestPath -Raw -Encoding UTF8 | ConvertFrom-Json $resultPath=$RequestPath+'.result.json' $progressPath=$RequestPath+'.progress.json' -$progress={param($run) Save-SuiteJson -Path $progressPath -Value $run} +$report=$null try { + Import-Module (Join-Path $PSScriptRoot 'Modules\Toolkit.Core.psm1') -Force -DisableNameChecking + Assert-SuiteUnlinkedPath $RequestPath + $request=Get-Content -LiteralPath $RequestPath -Raw -Encoding UTF8 | ConvertFrom-Json + $progress={param($run) Save-SuiteJson -Path $progressPath -Value $run} + Write-SuiteDiagnostic 'Worker startup' "Operation=$($request.Operation); DataRoot=$(Get-SuiteDataRoot); Request=$RequestPath" $data=$null; $report=$null; $message='Completed'; $status='Completed' switch ($request.Operation) { - 'Preview' { $data=@(Get-SuitePreview -ActionIds @($request.Ids)); $message='Preview only. No Windows settings changed.' } - 'Optimize' { $report=Invoke-SuiteRun -ActionIds @($request.Ids) -StartupIds @($request.StartupIds) -AppIds @($request.AppIds) -Progress $progress } + 'Preview' { $data=@(Get-SuitePreview -ActionIds @($request.Ids) -StartupIds @($request.StartupIds | Where-Object { $_ }) -AppIds @($request.AppIds | Where-Object { $_ })); $message='Preview only. No Windows settings changed.' } + 'Optimize' { $report=Invoke-SuiteRun -ActionIds @($request.Ids) -StartupIds @($request.StartupIds | Where-Object { $_ }) -AppIds @($request.AppIds | Where-Object { $_ }) -Progress $progress } 'Startup' { $data=@(Get-SuiteStartupItems); $message="$($data.Count) startup entries and third-party startup tasks found." } 'DisableStartup' { $report=Invoke-SuiteStartupDisable -Ids @($request.Ids) } 'Apps' { $data=@(Get-SuiteAppCatalog); $message="$($data.Count) optional consumer apps found." } @@ -19,7 +22,13 @@ try { 'History' { $data=@(Get-SuiteHistory); $message="$($data.Count) saved operations found." } 'Undo' { $report=Invoke-SuiteUndo -Path $request.Path } 'Repair' { $report=Invoke-SuiteRepair -Progress $progress } - 'RestorePoint' { Checkpoint-Computer -Description 'Windows PC Toolkit' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop; $message='Windows restore point created.' } + 'RestorePoint' { + $before=@(Get-ComputerRestorePoint -ErrorAction Stop | ForEach-Object SequenceNumber) + Checkpoint-Computer -Description 'Windows PC Toolkit' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop -WarningAction Stop + $created=@(Get-ComputerRestorePoint -ErrorAction Stop | Where-Object { $_.SequenceNumber -notin $before -and $_.Description -eq 'Windows PC Toolkit' }) + if (-not $created.Count) { throw 'Windows did not create a new toolkit restore point. Check System Protection and the restore-point frequency limit.' } + $message='Windows restore point created and verified.' + } 'DnsInfo' { $data=@(Get-NetAdapter -Physical -ErrorAction Stop | Where-Object Status -eq 'Up' | Select-Object Name,ifIndex,InterfaceDescription); $message='Active physical adapters loaded.' } 'Dns' { . (Resolve-SuitePath @('dns-encrypted-doh\DNS_Manager.ps1','Pc Privacy Guard\Optional DNS\DNS_Manager.ps1')) -Action Library -InterfaceIndex @($request.Ids | ForEach-Object { [int]$_ }) @@ -50,7 +59,16 @@ try { if ($report) { $status=$report.Status; $message=$report.Message } Save-SuiteJson $resultPath ([pscustomobject]@{Status=$status;Message=$message;Data=$data;Report=$report}) } catch { - if ($report) { $report.Status='Failed'; $report.Message=$_.Exception.Message; Save-SuiteJson $report.Path $report } - Save-SuiteJson $resultPath ([pscustomobject]@{Status='Failed';Message=$_.Exception.Message;Data=$null;Report=$null}) - throw + $failure=$_ + if (Get-Command Write-SuiteDiagnostic -ErrorAction SilentlyContinue) { Write-SuiteDiagnostic 'Worker failure' $failure.Exception.Message 'Error' } + if ($report) { + $report.Status='Failed'; $report.Message=$failure.Exception.Message + try { Add-SuiteStep $report $request.Operation 'Failed' $failure.Exception.Message } catch { Write-Warning $_.Exception.Message } + } + $result=[pscustomobject]@{Status='Failed';Message=$failure.Exception.Message;Data=$null;Report=$report} + # Keep startup/import failures visible even when the shared module could not load. + $json=$result | ConvertTo-Json -Depth 16 + if (Get-Command Assert-SuiteUnlinkedPath -ErrorAction SilentlyContinue) { Assert-SuiteUnlinkedPath $resultPath } + [IO.File]::WriteAllText($resultPath,$json,(New-Object Text.UTF8Encoding($false))) + throw $failure } diff --git a/Toolkit.ps1 b/Toolkit.ps1 index f431bbd..5d7f9f4 100644 --- a/Toolkit.ps1 +++ b/Toolkit.ps1 @@ -1,6 +1,7 @@ #requires -version 5.1 -param([switch]$SmokeTest,[string]$RenderPreview,[string]$ExpectedUserSid) +param([switch]$SmokeTest,[string]$RenderPreview,[string]$ExpectedUserSid,[switch]$DebugLog) $ErrorActionPreference='Stop' +if ($DebugLog) { $env:WINDOWS_PC_TOOLKIT_DEBUG='1' } trap { if ($SmokeTest -or $RenderPreview) { Write-Error $_; exit 1 } try { [void][Windows.MessageBox]::Show('Toolkit could not start. Extract the complete toolkit. '+$_.Exception.Message,'Windows PC Toolkit') } @@ -14,18 +15,20 @@ $principal=New-Object Security.Principal.WindowsPrincipal($identity) if (-not $SmokeTest -and -not $RenderPreview -and -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $escaped=$PSCommandPath.Replace("'","''") $command="& '$escaped' -ExpectedUserSid '$ExpectedUserSid'" + if ($env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1') { $command+=' -DebugLog' } $encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) $psi=New-Object Diagnostics.ProcessStartInfo $psi.FileName="$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" $psi.Arguments="-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy Bypass -EncodedCommand $encoded" $psi.UseShellExecute=$true; $psi.Verb='runas' - try { [void][Diagnostics.Process]::Start($psi) } + try { $elevated=[Diagnostics.Process]::Start($psi); $elevated.WaitForExit(); $code=$elevated.ExitCode; $elevated.Dispose(); exit $code } catch { [void][Windows.MessageBox]::Show('Administrator launch did not complete. '+$_.Exception.Message,'Windows PC Toolkit'); exit 1 } exit 0 } if (-not $SmokeTest -and -not $RenderPreview -and $identity.User.Value -ne $ExpectedUserSid) { throw 'Start the toolkit with administrator rights for your own account so settings apply to the correct Windows user.' } Import-Module (Join-Path $PSScriptRoot 'Modules\Toolkit.Core.psm1') -Force -DisableNameChecking +Write-SuiteDiagnostic 'Dashboard startup' "DataRoot=$(Get-SuiteDataRoot); Debug=$($env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1'); SmokeTest=$SmokeTest" [xml]$markup=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'Toolkit.xaml') -Raw -Encoding UTF8 $reader=New-Object Xml.XmlNodeReader($markup) $script:window=[Windows.Markup.XamlReader]::Load($reader) @@ -63,6 +66,9 @@ $script:controls.ProviderCombo.SelectedIndex=0 function Set-ToolkitBusy { param([bool]$Busy) foreach ($name in $script:operationButtons) { $script:controls[$name].IsEnabled=(-not $Busy) } + $script:controls.RecommendedButton.IsEnabled=(-not $Busy) + $script:controls.IncludeReviewed.IsEnabled=(-not $Busy) + foreach ($check in $script:optionChecks.Values) { $check.IsEnabled=(-not $Busy) } $script:controls.BusyBar.Visibility=if ($Busy) { 'Visible' } else { 'Collapsed' } } function Show-ToolkitPage { @@ -87,23 +93,26 @@ function Start-ToolkitOperation { Initialize-SuiteState $script:requestPath=Join-Path (Get-SuiteDataRoot) ('Requests\'+[guid]::NewGuid().ToString('N')+'.json') $startupIds=@(); $appIds=@($ConsumerAppIds) - if ($Operation -eq 'Optimize' -and $script:controls.IncludeReviewed.IsChecked) { + if ($Operation -in @('Optimize','Preview') -and $script:controls.IncludeReviewed.IsChecked) { $startupIds=@($script:controls.StartupGrid.SelectedItems | ForEach-Object Id) $appIds+=@($script:controls.AppsGrid.SelectedItems | ForEach-Object Id) } $request=[pscustomobject]@{Operation=$Operation;Ids=@($Ids);Path=$Path;DnsAction=$DnsAction;StartupIds=$startupIds;AppIds=$appIds} Save-SuiteJson $script:requestPath $request + Write-SuiteDiagnostic 'Dashboard operation' "$Operation; profile=$($Ids -join ','); startup=$($startupIds.Count); apps=$($appIds.Count)" $script:operation=$Operation; $script:progressStamp='' $script:controls.StatusText.Text="Running: $Operation" $script:controls.ResultText.Text=''; $script:controls.ResultText.Visibility='Visible' Set-ToolkitBusy $true $script:job=Start-Job -ScriptBlock { param($worker,$request) & $worker -RequestPath $request } -ArgumentList (Join-Path $PSScriptRoot 'Toolkit.Worker.ps1'),$script:requestPath - } catch { Set-ToolkitBusy $false; $script:controls.StatusText.Text='Failed: '+$_.Exception.Message } + } catch { Write-SuiteDiagnostic 'Dashboard operation' $_.Exception.Message 'Error'; Set-ToolkitBusy $false; $script:controls.StatusText.Text='Failed: '+$_.Exception.Message } } function Open-ToolkitConsole { param([string[]]$Candidates) $path=Resolve-SuitePath $Candidates - Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File "'+$path+'"') -WindowStyle Normal + $arguments='-NoProfile -STA -ExecutionPolicy Bypass -File "'+$path+'"' + if ([IO.Path]::GetFileName($path) -ne 'DNS_Manager.ps1') { $arguments+=' -ExpectedUserSid "'+(Get-SuiteIdentity)+'"' } + Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList $arguments -WindowStyle Normal } function Invoke-ToolkitButton { param($sender,$eventArgs) @@ -111,7 +120,7 @@ function Invoke-ToolkitButton { switch ($sender.Name) { 'OptimizeButton' { $ids=@($script:catalog | Where-Object { $script:optionChecks[$_.Id].IsChecked } | ForEach-Object Id) - if (-not $ids.Count) { $script:controls.StatusText.Text='Choose at least one action.'; return } + if (-not $ids.Count -and -not ($script:controls.IncludeReviewed.IsChecked -and ($script:controls.StartupGrid.SelectedItems.Count -or $script:controls.AppsGrid.SelectedItems.Count))) { $script:controls.StatusText.Text='Choose an action or reviewed startup/app selections.'; return } $apps=@() if ($ids -contains 'Debloat') { $apps+=@(Get-SuiteAppCatalog) } if ($script:controls.IncludeReviewed.IsChecked) { $apps+=@($script:controls.AppsGrid.SelectedItems) } @@ -160,7 +169,7 @@ function Invoke-ToolkitButton { 'DuckLink' { Start-Process 'https://github.com/itsfatduck/optimizerDuck' } 'SourceLink' { Start-Process 'https://github.com/SenjuWoo/windows-pc-toolkit' } } - } catch { $script:controls.StatusText.Text='Failed: '+$_.Exception.Message } + } catch { Write-SuiteDiagnostic 'Dashboard button' $_.Exception.Message 'Error'; $script:controls.StatusText.Text='Failed: '+$_.Exception.Message } } for ($i=0; $i -lt $script:navNames.Count; $i++) { $button=$script:controls[$script:navNames[$i]]; $button.Tag=$i @@ -210,6 +219,7 @@ $script:timer.Add_Tick({ } else { $script:controls.ResultText.Text=$result.Message } Remove-Job $script:job -Force; $script:job=$null; Set-ToolkitBusy $false } catch { + Write-SuiteDiagnostic 'Dashboard result' $_.Exception.Message 'Error' $script:controls.StatusText.Text='Failed: '+$_.Exception.Message if ($script:job -and $script:job.State -in @('Completed','Failed','Stopped')) { Remove-Job $script:job -Force; $script:job=$null; Set-ToolkitBusy $false } } diff --git a/Toolkit.xaml b/Toolkit.xaml index b163509..d258569 100644 --- a/Toolkit.xaml +++ b/Toolkit.xaml @@ -40,7 +40,7 @@ - + diff --git a/VALIDATION_REPORT.md b/VALIDATION_REPORT.md index 2b7c926..d4b0058 100644 --- a/VALIDATION_REPORT.md +++ b/VALIDATION_REPORT.md @@ -1,11 +1,11 @@ # Validation report -Windows PC Toolkit 3.0.0, local Windows 11 validation on October 1, 2026. +Windows PC Toolkit 3.0.1, local Windows 11 audit validation on October 10, 2026. [Whole-project findings](docs/AUDIT_2026-10-10.md). ## Observed results -- **114 regression and real-boundary checks passed** in Windows PowerShell 5.1, STA. -- **37 PowerShell files parsed**, and launcher/DoH/safety checks passed in `Validate_All.ps1`. +- **407 regression/boundary assertions passed locally** in Windows PowerShell 5.1, STA: base 116, Suite 24, Cleaner 53, DNS/privacy 41, repair/gaming 49, launchers 124. Administrative CI tokens omit 12 fixture-only principal-gate assertions, so the same suites report 395 assertions there. +- **43 PowerShell files parsed**, and launcher/DoH/safety checks passed in `Validate_All.ps1`. - Native WPF loaded **55 controls and 19 optimization choices** from a freshly copied package in a path containing spaces, an apostrophe and non-English text. - Actual WPF Preview button → Start-Job worker → DispatcherTimer → result text/control refresh completed. - Actual repair button/worker/timer completed native-process success, failure and restart-required scenarios; a disposable console executable replaced DISM/SFC. The real Windows stay-awake API enabled and released its system-required flag. @@ -16,6 +16,8 @@ Windows PC Toolkit 3.0.0, local Windows 11 validation on October 1, 2026. ## Tested boundaries +The added suites cover typed empty/single-item arrays and signed values, case-only conflicts, reviewed-only WPF optimization, linked state/cleanup/package paths, stale registry commands, AppX hash tampering, Recall post-reboot undo, restore-point no-op detection, startup/import failures, false-success console paths, service/boot/network rollback failures and originating-account guards. All 12 launchers execute actual CMD/PowerShell entrypoints with harmless target markers, exit 0/7 and Unicode/apostrophe/bracket/exclamation paths. A nonadministrative host substitutes only each fixture's principal gate; interactive UAC is not exercised. + Real disposable HKCU values and serialized journals cover String, ExpandString, Binary, MultiString, QWord, absent values and UTF-8 text. Exact undo, failed backup before mutation, intervening-user conflicts, repeated undo, interrupted pending writes, combined startup/profile journals and rollback after a partial registry action are exercised. Actual robocopy and SHA-256 checks run on disposable package files, followed by simulated AppX removal/re-registration and Recall feature APIs. Tests cover protected packages, failed backups blocking removal, default debloat preview, feature-state journaling/undo and already-disabled features. Privacy/AI recovery injection covers denied reads/writes and failed service restart without claiming successful restore. @@ -36,7 +38,7 @@ Published releases attach `release-provenance.json` with their exact commit, CI ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Validate_All.ps1 -powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_Toolkit.ps1 +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_All.ps1 powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\Toolkit.ps1 -RenderPreview .\docs\images\dashboard.png ``` diff --git a/Validate_All.ps1 b/Validate_All.ps1 index 6b3d5a3..c6f1df8 100644 --- a/Validate_All.ps1 +++ b/Validate_All.ps1 @@ -43,8 +43,8 @@ $dns=Get-Content -LiteralPath $dnsManager -Raw if($dns -match "Template='http://"){$failed=$true;Write-Host '[FAIL] Plain HTTP DoH template found.' -ForegroundColor Red} foreach($required in @('https://dns.quad9.net/dns-query','https://dns.adguard-dns.com/dns-query')){if($dns -notmatch [regex]::Escape($required)){$failed=$true;Write-Host "[FAIL] Missing DoH template: $required" -ForegroundColor Red}} if($dns -match '(?m)^\s*Mullvad\w*\s*='){$failed=$true;Write-Host '[FAIL] Retiring Mullvad profiles must not be offered for new applies.' -ForegroundColor Red} -foreach($required in @('9.9.9.9','149.112.112.112','2620:fe::fe','2620:fe::fe:9')){if($dns -notmatch [regex]::Escape($required)){$failed=$true;Write-Host "[FAIL] Missing Quad9 secure-profile address: $required" -ForegroundColor Red}} -if($dns -match [regex]::Escape("'2620:fe::9'")){$failed=$true;Write-Host '[FAIL] Quad9 address 2620:fe::9 belongs to a different service family.' -ForegroundColor Red} +foreach($required in @('9.9.9.9','149.112.112.112','2620:fe::fe','2620:fe::9')){if($dns -notmatch [regex]::Escape($required)){$failed=$true;Write-Host "[FAIL] Missing Quad9 secure-profile address: $required" -ForegroundColor Red}} +if($dns -match [regex]::Escape("'2620:fe::fe:9'")){$failed=$true;Write-Host '[FAIL] Incorrect Quad9 secondary IPv6 address.' -ForegroundColor Red} if($dns -notmatch 'Add-DohEntry[^\r\n]+-Fallback\s+\$false[^\r\n]+-Upgrade\s+\$true'){$failed=$true;Write-Host '[FAIL] DNS profile application does not force DoH with UDP fallback disabled.' -ForegroundColor Red} foreach($required in @('Get-StaticDnsServers','Automatic=','StaticIPv4','StaticIPv6','Schema=4','Set-AdapterDnsFamilies','TouchedServers')){if($dns -notmatch [regex]::Escape($required)){$failed=$true;Write-Host "[FAIL] Missing DNS mode-aware rollback marker: $required" -ForegroundColor Red}} $networkPath=Join-Path $root 'Modules\Network.State.psm1' diff --git a/dns-encrypted-doh/DNS_Encrypted_Manager.bat b/dns-encrypted-doh/DNS_Encrypted_Manager.bat index 8f61691..016a13a 100644 --- a/dns-encrypted-doh/DNS_Encrypted_Manager.bat +++ b/dns-encrypted-doh/DNS_Encrypted_Manager.bat @@ -1,35 +1,28 @@ @echo off -title Encrypted DNS Manager v2.2 -setlocal EnableExtensions +title Encrypted DNS Manager v2.2.1 +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action Menu set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/DNS_Manager.ps1 b/dns-encrypted-doh/DNS_Manager.ps1 index e0a386b..3bc9160 100644 --- a/dns-encrypted-doh/DNS_Manager.ps1 +++ b/dns-encrypted-doh/DNS_Manager.ps1 @@ -1,6 +1,6 @@ #requires -version 5.1 <#[ -Encrypted DNS Manager v2.2 +Encrypted DNS Manager v2.2.1 Configures one coherent provider at a time. Every configured DNS IP is paired with its official HTTPS DoH template, UDP fallback is disabled, and prior DNS state is saved for exact rollback. @@ -9,19 +9,32 @@ param( [ValidateSet('Menu','Library','Quad9','AdGuard','Mullvad','MullvadAdBlock','MullvadBase','MullvadExtended','MullvadFamily','MullvadAll','Verify','Restore','DHCP')] [string]$Action='Menu', [int[]]$InterfaceIndex, - [string]$SnapshotPath + [string]$SnapshotPath, + [switch]$DebugLog ) $ErrorActionPreference='Stop' $networkModule=@((Join-Path $PSScriptRoot '..\Modules\Network.State.psm1'),(Join-Path $PSScriptRoot '..\..\Modules\Network.State.psm1')) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $networkModule) { throw 'The shared network module is missing. Extract the complete toolkit.' } Import-Module $networkModule -Force -DisableNameChecking -$Version='2.2' +$Version='2.2.1' $StateRoot=Join-Path $env:ProgramData 'WindowsPCToolkit\EncryptedDNS' $SnapshotRoot=Join-Path $StateRoot 'Snapshots' +$DnsDebug=($DebugLog -or $env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1') +$DnsLog=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\DNS.log' +function Write-DnsDiagnostic { + param([string]$Kind,[string]$Message) + if (-not $DnsDebug -and $Kind -notin @('FAIL','WARN')) { return } + try { + if ($Message.Length -gt 4096) { $Message=$Message.Substring(0,4096) } + [IO.Directory]::CreateDirectory((Split-Path $DnsLog -Parent))|Out-Null + [pscustomobject]@{Time=(Get-Date).ToString('o');Version=$Version;Kind=$Kind;Message=$Message} | ConvertTo-Json -Compress | Add-Content -LiteralPath $DnsLog -Encoding UTF8 + } catch { Write-Warning "DNS diagnostic log could not be written: $($_.Exception.Message)" } +} +Write-DnsDiagnostic 'DEBUG' "Startup: action=$Action; interfaces=$($InterfaceIndex -join ','); snapshot=$SnapshotPath" $Providers=[ordered]@{ - Quad9=[pscustomobject]@{Name='Quad9 Secure'; V4=@('9.9.9.9','149.112.112.112'); V6=@('2620:fe::fe','2620:fe::fe:9'); Template='https://dns.quad9.net/dns-query'; LiveTest='Quad9'} + Quad9=[pscustomobject]@{Name='Quad9 Secure'; V4=@('9.9.9.9','149.112.112.112'); V6=@('2620:fe::fe','2620:fe::9'); Template='https://dns.quad9.net/dns-query'; LiveTest='Quad9'} AdGuard=[pscustomobject]@{Name='AdGuard DNS'; V4=@('94.140.14.14','94.140.15.15'); V6=@('2a10:50c0::ad1:ff','2a10:50c0::ad2:ff'); Template='https://dns.adguard-dns.com/dns-query'; LiveTest='Config'} } @@ -30,6 +43,7 @@ function Write-Status { $color=switch($Kind){'OK'{'Green'}'INFO'{'Cyan'}'WARN'{'Yellow'}default{'Red'}} $tag=switch($Kind){'OK'{'[OK]'}'INFO'{'[i]'}'WARN'{'[!!]'}default{'[X]'}} Write-Host (" {0} {1}" -f $tag,$Message) -ForegroundColor $color + Write-DnsDiagnostic $Kind $Message } function Test-Admin { $p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) @@ -63,26 +77,8 @@ function Get-StaticDnsServers { } function Get-DohStateForServer { param([string]$Server) - if(Get-Command Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue){ - $x=Get-DnsClientDohServerAddress -ErrorAction Stop | Where-Object ServerAddress -eq $Server | Select-Object -First 1 - if($x){return [pscustomobject]@{ServerAddress=$x.ServerAddress;DohTemplate=$x.DohTemplate;AllowFallbackToUdp=[bool]$x.AllowFallbackToUdp;AutoUpgrade=[bool]$x.AutoUpgrade}} - return $null - } - try { - $raw=@(& "$env:SystemRoot\System32\netsh.exe" dnsclient show encryption server=$Server 2>$null) - if($LASTEXITCODE -ne 0 -or -not $raw){return $null} - $text=$raw -join "`n" - $template=[regex]::Match($text,'https://\S+').Value.TrimEnd('.',',',';') - if(-not $template){return $null} - $fallback=$null - $upgrade=$null - if($text -match '(?im)UDP\s+fallback[^\r\n]*:\s*(Yes|True|Enabled)'){ $fallback=$true } - elseif($text -match '(?im)UDP\s+fallback[^\r\n]*:\s*(No|False|Disabled)'){ $fallback=$false } - if($text -match '(?im)Auto\s*upgrade[^\r\n]*:\s*(Yes|True|Enabled)'){ $upgrade=$true } - elseif($text -match '(?im)Auto\s*upgrade[^\r\n]*:\s*(No|False|Disabled)'){ $upgrade=$false } - if($null -eq $fallback -or $null -eq $upgrade){return $null} - return [pscustomobject]@{ServerAddress=$Server;DohTemplate=$template;AllowFallbackToUdp=[bool]$fallback;AutoUpgrade=[bool]$upgrade} - }catch{} + $x=Get-DnsClientDohServerAddress -ErrorAction Stop | Where-Object ServerAddress -eq $Server | Select-Object -First 1 + if($x){return [pscustomobject]@{ServerAddress=$x.ServerAddress;DohTemplate=$x.DohTemplate;AllowFallbackToUdp=[bool]$x.AllowFallbackToUdp;AutoUpgrade=[bool]$x.AutoUpgrade}} return $null } function New-DnsSnapshot { @@ -107,13 +103,13 @@ function New-DnsSnapshot { $enc=foreach($s in $TouchedServers){$state=Get-DohStateForServer $s;if($state){$state}} $currentProvider=$null $currentPath=Join-Path $StateRoot 'current_provider.json' - if(Test-Path -LiteralPath $currentPath){try{$currentProvider=Get-Content -LiteralPath $currentPath -Raw|ConvertFrom-Json}catch{}} + if(Test-Path -LiteralPath $currentPath){$currentProvider=Get-Content -LiteralPath $currentPath -Raw -Encoding UTF8|ConvertFrom-Json} $obj=[pscustomobject]@{Schema=4;Created=(Get-Date).ToString('o');Computer=$env:COMPUTERNAME;Adapters=@($adapters);Encryption=@($enc);TouchedServers=@($TouchedServers);CurrentProvider=$currentProvider} - $path=Join-Path $SnapshotRoot ("dns_{0}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss_fff')) + $path=Join-Path $SnapshotRoot ("dns_{0}_{1}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss_fff'),[guid]::NewGuid().ToString('N')) $obj|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path -Encoding UTF8 $verified=Get-Content -LiteralPath $path -Raw | ConvertFrom-Json - if (@($verified.Adapters).Count -ne @($adapters).Count) { throw 'DNS backup verification failed; no DNS settings were changed.' } - Set-Content -LiteralPath (Join-Path $StateRoot 'latest_snapshot.txt') -Value $path -Encoding ASCII + if ((ConvertTo-Json -InputObject $verified -Depth 8 -Compress) -ne (ConvertTo-Json -InputObject $obj -Depth 8 -Compress)) { throw 'DNS backup verification failed; no DNS settings were changed.' } + Set-Content -LiteralPath (Join-Path $StateRoot 'latest_snapshot.txt') -Value $path -Encoding UTF8 Write-Status OK ("DNS snapshot saved: {0}" -f $path) return $path } @@ -125,28 +121,22 @@ function Get-LatestSnapshot { } function Remove-DohEntry { param([string]$Server) - if(Get-Command Remove-DnsClientDohServerAddress -ErrorAction SilentlyContinue){ - if (Get-DohStateForServer $Server) { Remove-DnsClientDohServerAddress -ServerAddress $Server -ErrorAction Stop } - return - } - & "$env:SystemRoot\System32\netsh.exe" dnsclient delete encryption server=$Server 2>$null|Out-Null + if (Get-DohStateForServer $Server) { Remove-DnsClientDohServerAddress -ServerAddress $Server -ErrorAction Stop } + if (Get-DohStateForServer $Server) { throw "DoH removal read-back failed for $Server." } } function Add-DohEntry { param([string]$Server,[string]$Template,[bool]$Fallback=$false,[bool]$Upgrade=$true) - if(Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue){ - if (Get-DohStateForServer $Server) { Set-DnsClientDohServerAddress -ServerAddress $Server -DohTemplate $Template -AllowFallbackToUdp $Fallback -AutoUpgrade $Upgrade -ErrorAction Stop } - else { Add-DnsClientDohServerAddress -ServerAddress $Server -DohTemplate $Template -AllowFallbackToUdp $Fallback -AutoUpgrade $Upgrade -ErrorAction Stop } - }else{ - $fallbackText=if($Fallback){'yes'}else{'no'};$upgradeText=if($Upgrade){'yes'}else{'no'} - $verb=if(Get-DohStateForServer $Server){'set'}else{'add'} - $result=& "$env:SystemRoot\System32\netsh.exe" dnsclient $verb encryption server=$Server dohtemplate=$Template autoupgrade=$upgradeText udpfallback=$fallbackText 2>&1 - if($LASTEXITCODE -ne 0){throw "Could not register DoH for $Server. $($result -join ' ')"} - } + if (Get-DohStateForServer $Server) { Set-DnsClientDohServerAddress -ServerAddress $Server -DohTemplate $Template -AllowFallbackToUdp $Fallback -AutoUpgrade $Upgrade -ErrorAction Stop } + else { Add-DnsClientDohServerAddress -ServerAddress $Server -DohTemplate $Template -AllowFallbackToUdp $Fallback -AutoUpgrade $Upgrade -ErrorAction Stop } + $actual=Get-DohStateForServer $Server + if (-not $actual -or $actual.DohTemplate -ne $Template -or $actual.AllowFallbackToUdp -ne $Fallback -or $actual.AutoUpgrade -ne $Upgrade) { throw "DoH write read-back failed for $Server." } } function Test-WindowsDohSupport { $build=[int](Get-CimInstance Win32_OperatingSystem).BuildNumber if($build -lt 22000){throw 'This encrypted DNS profile requires Windows 11. Windows 10 does not provide the same supported per-server DoH client configuration.'} - if(-not(Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue) -and -not(Test-Path "$env:SystemRoot\System32\netsh.exe")){throw 'No supported Windows DoH configuration interface was found.'} + foreach ($command in @('Get-DnsClientDohServerAddress','Add-DnsClientDohServerAddress','Set-DnsClientDohServerAddress','Remove-DnsClientDohServerAddress')) { + if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { throw "The supported Windows DoH interface is unavailable: $command. No DNS settings were changed." } + } } function Set-Provider { param([string]$Key) @@ -216,8 +206,7 @@ function Test-Configuration { return [bool]$allGood } function Show-EncryptionTable { - if(Get-Command Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue){Get-DnsClientDohServerAddress|Format-Table ServerAddress,DohTemplate,AllowFallbackToUdp,AutoUpgrade -AutoSize} - else{& "$env:SystemRoot\System32\netsh.exe" dnsclient show encryption} + Get-DnsClientDohServerAddress -ErrorAction Stop|Format-Table ServerAddress,DohTemplate,AllowFallbackToUdp,AutoUpgrade -AutoSize } function Restore-Snapshot { param([string]$Path) @@ -226,10 +215,16 @@ function Restore-Snapshot { $snap=Get-Content -LiteralPath $path -Raw|ConvertFrom-Json if ($snap.Schema -notin @(2,3,4)) { throw 'Unsupported DNS backup schema.' } if ($snap.PSObject.Properties.Name -contains 'Computer' -and $snap.Computer -ne $env:COMPUTERNAME) { throw 'This DNS backup belongs to another computer.' } + if (-not @($snap.Adapters).Count) { throw 'DNS backup contains no adapters; no DNS settings were changed.' } $availableAdapters=@(Get-NetAdapter -ErrorAction Stop) foreach ($a in @($snap.Adapters)) { if ($a.PSObject.Properties.Name -notcontains 'InterfaceGuid') { throw 'This legacy backup has no stable adapter identity; no DNS settings were changed.' } if (-not @($availableAdapters | Where-Object { $_.InterfaceGuid.ToString().Trim('{}') -eq $a.InterfaceGuid.ToString().Trim('{}') }).Count) { throw "Original adapter no longer exists: $($a.Name). No DNS settings were changed." } + if ($a.PSObject.Properties.Name -contains 'Automatic') { + if ($a.Automatic -isnot [bool] -or $a.PSObject.Properties.Name -notcontains 'StaticIPv4' -or $a.PSObject.Properties.Name -notcontains 'StaticIPv6') { throw 'DNS backup has invalid automatic/static state; no DNS settings were changed.' } + $addresses=@(@($a.StaticIPv4)+@($a.StaticIPv6) | Where-Object {$_}) + if (([bool]$a.Automatic -and $addresses.Count) -or (-not [bool]$a.Automatic -and -not $addresses.Count)) { throw "DNS backup has inconsistent address modes for $($a.Name); no DNS settings were changed." } + } } $touched=if($snap.PSObject.Properties.Name -contains 'TouchedServers'){@($snap.TouchedServers)}else{@(Get-TargetServers)} foreach($server in $touched | Where-Object { @($snap.Encryption | ForEach-Object ServerAddress) -notcontains $_ }){Remove-DohEntry $server} @@ -278,7 +273,12 @@ function Reset-Dhcp { Assert-Admin;Initialize-State $snapshotPath=New-DnsSnapshot try{ - foreach($a in Get-Adapters){Set-AdapterDnsFamilies -Index $a.ifIndex;Write-Status OK ("{0}: DNS returned to DHCP/automatic" -f $a.Name)} + foreach($a in Get-Adapters){ + Set-AdapterDnsFamilies -Index $a.ifIndex + $actual=Get-StaticDnsServers -Adapter $a + if (@($actual.IPv4).Count -or @($actual.IPv6).Count) { throw "Automatic DNS read-back failed for $($a.Name)." } + Write-Status OK ("{0}: DNS returned to DHCP/automatic" -f $a.Name) + } Remove-Item -LiteralPath (Join-Path $StateRoot 'current_provider.json') -ErrorAction SilentlyContinue Clear-DnsClientCache -ErrorAction SilentlyContinue Write-Status OK 'Automatic DNS restored. Global DoH entries were preserved for other adapters.' @@ -310,15 +310,17 @@ function Show-Menu { if ($Action -eq 'Library') { return } if($Action -ne 'Menu'){ + try { switch($Action){ 'Quad9'{Set-Provider Quad9}'AdGuard'{Set-Provider AdGuard}'Mullvad'{Set-Provider Mullvad}'MullvadAdBlock'{Set-Provider MullvadAdBlock} 'MullvadBase'{Set-Provider MullvadBase}'MullvadExtended'{Set-Provider MullvadExtended}'MullvadFamily'{Set-Provider MullvadFamily}'MullvadAll'{Set-Provider MullvadAll} 'Verify'{if (-not (Test-Configuration)) { exit 1 }}'Restore'{Restore-Snapshot -Path $SnapshotPath}'DHCP'{Reset-Dhcp} } + } catch { Write-DnsDiagnostic 'FAIL' $_.Exception.Message; throw } exit } while($true){ - Show-Menu;$c=(Read-Host ' Select').Trim().ToUpperInvariant() + Show-Menu;$c=(Read-Host ' Select').Trim().ToUpperInvariant();Write-DnsDiagnostic 'DEBUG' "Selected action: $c" try{switch($c){'1'{Set-Provider Quad9}'2'{Set-Provider AdGuard}'V'{Test-Configuration|Out-Null}'R'{Restore-Snapshot}'D'{Reset-Dhcp}'0'{break}default{Write-Status WARN 'Invalid selection.'}}}catch{Write-Status FAIL $_.Exception.Message} if($c -eq '0'){break};Write-Host '';Read-Host ' Press Enter to continue'|Out-Null } diff --git a/dns-encrypted-doh/DNS_Restore_Previous.bat b/dns-encrypted-doh/DNS_Restore_Previous.bat index 116566e..0d58c52 100644 --- a/dns-encrypted-doh/DNS_Restore_Previous.bat +++ b/dns-encrypted-doh/DNS_Restore_Previous.bat @@ -1,35 +1,28 @@ @echo off title DNS: Restore Previous -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action Restore set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/DNS_Revert_DHCP.bat b/dns-encrypted-doh/DNS_Revert_DHCP.bat index 5717cc9..823b4f9 100644 --- a/dns-encrypted-doh/DNS_Revert_DHCP.bat +++ b/dns-encrypted-doh/DNS_Revert_DHCP.bat @@ -1,35 +1,28 @@ @echo off title DNS: Revert to DHCP -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action DHCP set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/DNS_Set_Mullvad_AdBlock.bat b/dns-encrypted-doh/DNS_Set_Mullvad_AdBlock.bat index 965770d..c197206 100644 --- a/dns-encrypted-doh/DNS_Set_Mullvad_AdBlock.bat +++ b/dns-encrypted-doh/DNS_Set_Mullvad_AdBlock.bat @@ -1,36 +1,28 @@ @echo off title DNS: Choose a supported provider -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" -echo Mullvad public DNS is retiring. Choose Quad9 or AdGuard in the menu. "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action Menu set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/DNS_Set_Quad9.bat b/dns-encrypted-doh/DNS_Set_Quad9.bat index 4558926..1e639d7 100644 --- a/dns-encrypted-doh/DNS_Set_Quad9.bat +++ b/dns-encrypted-doh/DNS_Set_Quad9.bat @@ -1,35 +1,28 @@ @echo off title DNS: Apply Quad9 -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action Quad9 set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/DNS_Set_Quad9_Mullvad.bat b/dns-encrypted-doh/DNS_Set_Quad9_Mullvad.bat index 0c2b8c6..f172539 100644 --- a/dns-encrypted-doh/DNS_Set_Quad9_Mullvad.bat +++ b/dns-encrypted-doh/DNS_Set_Quad9_Mullvad.bat @@ -1,35 +1,28 @@ @echo off title DNS: Open Manager -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\DNSManager_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\DNSManager_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" "%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0DNS_Manager.ps1" -Action Menu set "RC=%ERRORLEVEL%" -echo [%date% %time%] DNS_Manager.ps1 exited with %RC% >> "%LOG%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) +exit /b %RC% + +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 +set "RC=%ERRORLEVEL%" if not "%RC%"=="0" ( - echo. - echo Encrypted DNS Manager exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/dns-encrypted-doh/README.md b/dns-encrypted-doh/README.md index b0ba5c4..341ed21 100644 --- a/dns-encrypted-doh/README.md +++ b/dns-encrypted-doh/README.md @@ -1,4 +1,4 @@ -# Encrypted DNS Manager 2.2 +# Encrypted DNS Manager 2.2.1 Windows 11 DNS-over-HTTPS using one provider per physical adapter. Use the [dashboard](../README.md) or `DNS_Encrypted_Manager.bat`. Extract the full toolkit, including `Modules\Network.State.psm1`. @@ -7,7 +7,7 @@ Windows 11 DNS-over-HTTPS using one provider per physical adapter. Use the [dash | Quad9 Secure | `9.9.9.9`, `149.112.112.112` | `https://dns.quad9.net/dns-query` | | AdGuard DNS | `94.140.14.14`, `94.140.15.15` | `https://dns.adguard-dns.com/dns-query` | -Matching IPv6 servers are registered; adapters receive them only with usable IPv6 and a default IPv6 route. Quad9 is the recommended ongoing profile. [Quad9](https://docs.quad9.net/services/), [AdGuard](https://adguard-dns.io/en/public-dns.html). +Matching IPv6 servers are registered; adapters receive them only with usable IPv6 and a default IPv6 route. Quad9 Secure uses `2620:fe::fe` and `2620:fe::9`. Quad9 is the recommended ongoing profile. [Quad9](https://docs.quad9.net/services/), [AdGuard](https://adguard-dns.io/en/public-dns.html). **Mullvad public DNS retires November 2, 2026.** New Mullvad applies and menu choices are removed now; backup restore remains available. [Official announcement](https://mullvad.net/en/blog/2026/9/3/shutting-down-our-public-encrypted-dns-servers-and-sponsoring-quad9-instead). @@ -20,6 +20,9 @@ Matching IPv6 servers are registered; adapters receive them only with usable IPv - Verify addresses, flags, templates and resolution. Quad9 also requires its live TXT result to report `doh`; unavailable tests fail. - Attempt rollback after failed apply and report rollback failures. - Automatic DNS preserves global DoH entries used by other adapters. +- Validate both requested address families before changing either; verify DoH writes/removals and automatic-DNS resets by reading state back. + +The supported Windows 11 `DnsClient` DoH cmdlets are required. An unavailable API or unreadable backup state stops changes. Localized `netsh` text is not used to guess encryption flags. AdGuard checks validate configuration/resolution without claiming Quad9's live attestation. DoH does not change public IP or replace a VPN. @@ -34,3 +37,5 @@ Snapshots: `%ProgramData%\WindowsPCToolkit\EncryptedDNS\Snapshots`. Schema 4 tra ``` Replace `12` with your physical adapter index. Historical `DNS_Set_Quad9_Mullvad.bat` and `DNS_Set_Mullvad_AdBlock.bat` open the supported-provider menu. They do not silently change your provider. + +Errors are recorded in `%LOCALAPPDATA%\WindowsPCToolkit\Logs\DNS.log`. Add `-DebugLog`, or set `WINDOWS_PC_TOOLKIT_DEBUG=1` before launching, to include startup version/configuration and operation events. Detailed logging is off by default. diff --git a/docs/AUDIT_2026-10-10.md b/docs/AUDIT_2026-10-10.md new file mode 100644 index 0000000..3b2c9ed --- /dev/null +++ b/docs/AUDIT_2026-10-10.md @@ -0,0 +1,38 @@ +# Whole-project correctness audit, 2026-10-10 + +Toolkit 3.0.1 audits the existing implementation before expanding its feature set. The baseline was `b73f3887b7dbca7453bfea5e77512660dceb4b68` (public 3.0.0). This is a source update and local candidate, not a new published release. No optimizer/debloat features or runtime dependencies were added. + +## Coverage + +| Project area | Review and verification | +| --- | --- | +| `Toolkit.ps1`, `Toolkit.xaml`, `Toolkit.Run.ps1`, `Toolkit.Worker.ps1`, `START_TOOLKIT.bat`, `Modules/Toolkit.Core.psm1` | All pages, named handlers, CLI/default selection, background requests/results, native repair, startup/task/app catalogs, before-write journals, recovery paths, native process quoting, state paths, diagnostics and WPF rendering. | +| `pc-cleaner/**` (script, launcher, validator, README) | All cleanup categories, scan/delete boundaries, registry review/removal/restore, console selections and logs. [Detailed audit](audit-cleaner.md). | +| `pc-corruption-fixer/**` and `pc-gaming-optimizer/**` | All repair/health/report actions, gaming/VR actions, shared optimizer module, numbered compatibility wrappers, launchers and documentation. [Detailed audit](audit-repair-gaming.md). | +| `dns-encrypted-doh/**`, `pc-privacy-guard/**`, `Modules/Network.State.psm1` | Provider data, every DNS wrapper, typed Windows API contracts, adapter/family mode snapshots, preflight/apply/verify/rollback, privacy profiles and restore. [Detailed audit](audit-dns-privacy.md). | +| `tests/**`, `Validate_All.ps1`, component validators | Reviewed what tests actually intercept; added independent failure-path checks and one aggregate entrypoint. Corrected a validator that enforced wrong Quad9 data. | +| `.github/**`, `.gitignore`, contributor/security/license/conduct files | Both-layout CI, parser/analyzer/data gates, pinned first-party action versions and alert queries; no unrelated policy changes. | +| Root/component README, changelog, coverage map, release notes, historical design, SVG/PNG | Updated current contracts and versions; kept historical release notes; marked the old design as historical. Rendered actual current WPF output. Coverage map remains deliberately narrower than optimizerDuck. | + +## Shared dashboard/core defects repaired + +- Registry equality ignored case, skipping case-only writes and losing conflict detection. Compare literal serialized values case-sensitively. +- Preview omitted reviewed startup/app choices; Optimize refused reviewed-only runs. Include those selections and freeze profile controls during operations. +- Worker import/JSON failures escaped before the result handler, while partial failures dropped their report. Return a visible failure envelope and retain reports/causes. +- Recovery trusted JSON-supplied report paths, displayed foreign-computer reports, and claimed success for repair/cache-only undo. Use the actual local report path and reject nonreversible journals. +- Existing linked state directories could redirect writes. Check existing ancestors before state/package reads and writes. App reinstall now checks persisted file hashes, containment and all linked descendants before registration. +- Task lookup could expand wildcards to unrelated tasks. Filter exact task identity and pass the resulting CIM object to enable/disable. Already-disabled tasks are skipped. +- Shared registry care used a stale missing-executable scan. Recheck the current command before removal so replacements survive review. +- Recall undo rejected the ordinary Disabled state following a DisablePending reboot. Record the expected completed state and support older pending-state journals. +- Restore-point creation assumed success from a no-output command. Treat warnings as failures and require a new matching restore-point sequence number. +- DISM repair lacked explicit `/NoRestart` and continued after restart-required completion. Defer remaining repair commands until Windows restarts. +- Root launcher lost the child exit status; standalone launchers had path/UAC/service-check defects. Preserve exit codes and originating user identity for HKCU-changing consoles. Alternate administrator credentials abort before bootstrap. +- Add bounded diagnostics with version, configuration, run/action and failure cause. Debug defaults off; operation reports retain normal results. + +Quad9 Secure secondary IPv6 is `2620:fe::9`, as verified against [Quad9's current service table](https://docs.quad9.net/services/). The prior `2620:fe::fe:9` assertion was wrong. DNS writes now require supported typed Windows DoH cmdlets instead of interpreting localized netsh text. [Microsoft DoH controls](https://techcommunity.microsoft.com/blog/networkingblog/windows-insiders-gain-new-dns-over-https-controls/2494644), [scheduled-task wildcard contract](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/get-scheduledtask), [restore-point behavior](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/checkpoint-computer?view=powershell-5.1). + +## Evidence and limits + +Run `tests/Test_All.ps1` in Windows PowerShell 5.1 STA; CI runs it in canonical and installed friendly layouts. Checks use real disposable HKCU/files/junctions, native CMD/PowerShell processes, actual WPF/jobs/timer, read-only Windows inventories, real stay-awake/SFC-help and native-process fixtures. Mutation commands for DNS, servicing, packages, services, storage and restore points are intercepted. Launcher targets are harmless marker scripts; the fixture substitutes the principal gate when the host token is not administrative, so interactive UAC acceptance is not proven. + +See [validation evidence](../VALIDATION_REPORT.md) and the exact commit's CI checks. This audit does not certify every Windows build, live corruption repair, AppX re-registration, provider connectivity or gaming performance. No live optimizer/debloat/DNS/repair action was applied to the host. Cleanup can race external filesystem changes; locked/inaccessible files are skipped. Old console snapshots require review and may replace later manual changes. Older AppX backups without hashes use Store reinstall rather than automatic registration. These are documented limits, not a claim that the app is perfect. diff --git a/docs/audit-cleaner.md b/docs/audit-cleaner.md new file mode 100644 index 0000000..1d8e2e0 --- /dev/null +++ b/docs/audit-cleaner.md @@ -0,0 +1,30 @@ +# PC Cleaner audit, 2026-10-10 + +Scope: all four `pc-cleaner` source/launcher/documentation files; callers in the shared Toolkit module; registry journal and undo dependencies; the existing toolkit regression coverage. No new debloat features were added. + +## Confirmed defects and repairs + +| Defect and mechanism | Repair and evidence | +| --- | --- | +| Windows.old was a Strict category whose executor took ownership, rewrote ACLs and recursively deleted the previous installation. This contradicted the documented report-only promise and removed upgrade rollback data. | Windows.old is report-only. Removed ownership/deletion branches. The executor refuses every protected report-only category by ID, including old descriptors with Strict/Special fields. | +| The deletion guard checked only the leaf reparse bit. A normal file or directory below a junction could therefore reach outside the cache location. | Check every existing ancestor before deleting or enumerating a root. Native junction fixtures prove normal leaves below links survive direct, category and Temp cleanup. Common local model-store names are protected too. | +| Temp scan counted recent/protected files while actual cleanup accepted only aged files. A file refreshed after enumeration was not rechecked by the console. | Scan and cleanup use the same candidate enumeration; cleanup rechecks write/access ages immediately before removal. Native aged, accessed, refreshed and protected files exercise this boundary. | +| Report-only rows in the main table were omitted entirely from scan output and kept totals; backup review counted only files with a surviving original. | Render all report-only categories and include both backup candidate groups. No backup file is offered to deletion by the executor. | +| Wildcard profile paths interpreted literal brackets in parent directory names. | Escape literal path metacharacters while retaining the intended star wildcard. Real bracket/apostrophe filesystem fixtures cover directories and files. | +| Delivery Optimization assumed all pre-existing files were removed on any successful call and swallowed exceptions as successful-looking results. | Measure the reduction after the official cmdlet, preserve a Failed status and cause on error, show failures in console summaries and persist CompletedWithErrors in run reports. Native cache deletion is faked in these tests. | +| Registry care validated the scanned startup command but removed the current registry value, so a replacement made during review could be removed. Failure after a partial registry write lacked immediate rollback. | Read the current command before deciding; use existing typed Suite journals; roll back entries added by a failed individual repair. Native owned HKCU tests preserve a new valid executable, remove a missing target and undo exactly. | +| Unquoted executable paths with spaces could be interpreted as one missing path despite Windows trying multiple candidates. Prefix filtering also hid legitimate value names starting with PS. | Retain ambiguous commands for review; skip only known provider metadata names. [Microsoft's CreateProcess contract](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw) documents the ambiguity. | +| Legacy backup restore overwrote existing replacement shortcut files and imported entire registry snapshots without disclosing that newer edits could be replaced. | Preserve existing destination files; require explicit confirmation before importing whole legacy registry keys. | +| Logs used second-resolution filenames, overwriting evidence from rapid repeated runs. | Append a GUID; native repeated-run tests verify both reports remain. Add optional bounded diagnostics, quiet on library import. | +| Launcher elevation interpolated the batch path into single-quoted PowerShell, failing on apostrophes; `net session` required the Server service; a timed flag could falsely report UAC failure. Alternate administrator credentials could target another account's files and HKCU. | Pass the launch path through an environment variable, check the Windows principal directly, wait for the elevated child, propagate its actual exit code and validate the originating user SID before bootstrap/action. The launcher audit tests native path/argument boundaries separately. | +| Custom category ranges could allocate an enormous integer range or throw on integer overflow. | Parse and bound both endpoints before creating the range; oversized native console input test reaches no cleanup call. | + +## Verification + +`powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File tests\Test_AuditCleaner.ps1` passes **53 checks** on native Windows PowerShell 5.1. It uses real disposable files, real directory junctions, an exclusively locked file, an owned HKCU key, typed Suite journal undo and an injected interruption after actual test-key removal. Delivery Optimization and console inputs are faked. The original cleaner self-test and parser gate also pass. + +Fixtures clean up their own files, links and registry key. The restricted sandbox cannot create junctions; the native fixture run used the ordinary user process outside that sandbox, without requiring real cleanup or system repair. + +## Remaining limits + +Cleanup is not atomic with another process renaming filesystem directories between checks and deletion. Permission-denied and locked paths are skipped; scans are estimates for accessible files, and a cache may change during measurement. A partially failed recursive removal can delete some ordinary disposable children before it encounters a locked child. File/cache deletion has no undo. Legacy whole-key registry imports cannot detect every intervening edit, hence their explicit confirmation. No real user cache, Windows.old, app package, service or Windows Update data was cleaned during this audit; no live performance benefit is claimed. diff --git a/docs/audit-dns-privacy.md b/docs/audit-dns-privacy.md new file mode 100644 index 0000000..428119e --- /dev/null +++ b/docs/audit-dns-privacy.md @@ -0,0 +1,50 @@ +# DNS and Privacy Guard audit + +Audit date: 2026-10-10. Scope: every file in `dns-encrypted-doh/` and `pc-privacy-guard/`, plus `Modules/Network.State.psm1`. Callers in `Toolkit.Worker.ps1`, `Modules/Toolkit.Core.psm1`, `pc-corruption-fixer/PC_Fixer.ps1`, gaming-tool path resolution, existing regression tests and `Validate_All.ps1` were traced. No new optimization or debloat features were added. + +## Confirmed defects and corrections + +| Defect | Mechanism and correction | +| --- | --- | +| Every fresh Privacy Guard undo was rejected | Snapshot creation omitted `Computer` and `User`, but restore required both. New verified snapshots contain computer, account and SID. Existing identity-less backups require explicit confirmation; foreign identities fail before writes. | +| Incorrect Quad9 IPv6 address | The provider used `2620:fe::fe:9`; the current official Secure service table and Windows 11 guide specify `2620:fe::9`. Corrected the provider data and added a regression assertion. The repository's previous validator incorrectly enforced the bad value; the whole-project integration corrects that validator. | +| Privacy backups could omit state that strict mode changed | Service and task inventory errors were suppressed. Inventory now fails closed, captures actual `Settings.Enabled`, records delayed automatic startup, and rejects transient service states that cannot be restored exactly. Snapshot JSON is read back before its pointer is published. | +| Strict privacy silently reported denied writes as successful | Service/task mutation used `SilentlyContinue`, then printed OK unconditionally. Errors now propagate, state is checked after successful writes, and only captured services/tasks are mutated. Saved unsupported service states fail restore preflight. | +| Registry writes lacked verification and unsupported types became strings | Exact writes now validate the requested type and check the resulting type/value. Array assignment preserves empty and single-element Binary/MultiString values through the read-back and JSON undo round trip. | +| DNS family errors could partially change an adapter | The shared setter changed IPv4 before discovering that requested IPv6 was unavailable. It now reads and validates both address-family objects before either mutation, and rejects empty adapter inventories. | +| Unsafe localized DoH fallback could destroy prior state | English-only `netsh` parsing returned null for unreadable flags, indistinguishable from an absent entry; deletion exit status was ignored. Supported typed Windows 11 DoH cmdlets now read/write the state, and unavailable APIs fail before profile writes. Existing entries are still updated in place. DoH writes and removals are read back. | +| DNS restore and DHCP reset could claim success incorrectly | Malformed static/automatic backup modes are rejected before encryption/adapter writes. DHCP reset now checks that both static server lists are empty and attempts the saved rollback on verification failure. Unreadable provider metadata aborts backup creation. Snapshots use unique filenames and UTF-8 pointers. | +| Standalone launchers broke on apostrophe paths or disabled Server service | Paths were injected into quoted PowerShell source; `net session` incorrectly stood in for administrator identity; the six-second sentinel could report a slow UAC response as failure. All seven scoped launchers now carry the exact path in an environment variable, disable inherited delayed expansion, check the Windows administrator role, wait for the elevated child and return its exit status. Privacy carries the original account SID through elevation and rejects alternate-account HKCU changes before state initialization. | + +## Verification + +`tests/Test_AuditDnsPrivacy.ps1` runs in Windows PowerShell 5.1. It uses an isolated HKCU test key and temporary JSON files. DNS, service and task mutations are intercepted before test execution. Coverage includes real typed registry round trips, fresh and legacy identity checks, inventory failures, strict mutation failures, per-family preflight, provider data, ignored DoH writes/deletes, malformed backups and DHCP rollback. The whole-project CI also runs the original regression suite and the friendly-folder layout. + +```powershell +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File tests\Test_AuditDnsPrivacy.ps1 +``` + +The focused suite has **41 checks**, including quiet-default/debug-toggle, bounded failure-log, exact registry string-case and native foreign-SID entrypoint checks. Whole-project integration also checks the batch launchers using disposable native fixtures. These tests do not apply DNS/privacy profiles to the development PC. + +## Version and logging + +Encrypted DNS Manager is **2.2.1** (`DNS_Manager.ps1` header and version, `DNS_Encrypted_Manager.bat` title, DNS README). Privacy Guard is **2.1.1** (`PC_Privacy.ps1` header and version, `Run_As_Admin.bat` title, Privacy README). Shared network fixes are included with the toolkit patch version. + +Detailed logging is quiet by default. Enable `-DebugLog` or `WINDOWS_PC_TOOLKIT_DEBUG=1` for startup version, resolved configuration and operation events. Errors are retained even when detailed logging is disabled: + +- `%LOCALAPPDATA%\WindowsPCToolkit\Logs\DNS.log` +- `%LOCALAPPDATA%\WindowsPCToolkit\Logs\PrivacyGuard.log` + +## Primary-source checks + +- [Microsoft Set-DnsClientServerAddress](https://learn.microsoft.com/en-us/powershell/module/dnsclient/set-dnsclientserveraddress): per-family CIM input and reset-to-DHCP contract. +- [Microsoft Get-DnsClientDohServerAddress](https://learn.microsoft.com/en-us/powershell/module/dnsclient/get-dnsclientdohserveraddress): typed encryption configuration inventory. +- [Microsoft Set-DnsClientDohServerAddress](https://learn.microsoft.com/en-us/powershell/module/dnsclient/set-dnsclientdohserveraddress): existing-entry updates, automatic upgrade and UDP fallback semantics. +- [Quad9 services](https://docs.quad9.net/services/) and [Windows 11 encrypted DNS guide](https://docs.quad9.net/Setup_Guides/Windows/Windows_11_%28Encrypted%29/): Secure addresses and HTTPS template. +- [Quad9 protocol test](https://docs.quad9.net/FAQs/): `proto.on.quad9.net` TXT transport result. +- [AdGuard public DNS](https://adguard-dns.io/en/public-dns.html): matching default IPv4/IPv6 servers and HTTPS template. +- [Mullvad retirement announcement](https://mullvad.net/en/blog/shutting-down-our-public-encrypted-dns-servers-and-sponsoring-quad9-instead): migration before November 2, 2026; retired profiles remain excluded from new applies. + +## Limits + +No live DNS switch, service stop, scheduled-task change or production privacy policy was applied. Tests establish orchestration, read-back and rollback behavior; they do not certify every Windows edition, managed policy or provider/network combination. Quad9's live test checks the resolver path Windows actually used; it does not attest every independent adapter path. AdGuard still has no equivalent transport-attestation claim. Legacy snapshots can restore only the fields their original version recorded. Windows policy support can vary by edition and management settings. diff --git a/docs/audit-repair-gaming.md b/docs/audit-repair-gaming.md new file mode 100644 index 0000000..701e160 --- /dev/null +++ b/docs/audit-repair-gaming.md @@ -0,0 +1,38 @@ +# Repair and gaming audit + +Reviewed on 2026-10-10: all files in `pc-corruption-fixer` and `pc-gaming-optimizer`, including the full repair console, shared gaming module, numbered wrappers and batch launchers. This audit adds bug fixes, not new debloat features. + +## Corrected defects + +- Repair summary called `ContainsKey` on an `OrderedDictionary`, causing an exception at exit. Summary now uses actual results without that invalid method and does not turn warnings/skips into unconditional success. +- Quick Health Scan guessed SFC integrity from numeric exit codes, used English DISM output as a portable verdict, and flagged an idle Manual Windows Update service as broken. It now displays native integrity verdicts, distinguishes command failures and accepts demand-start idle services. +- SFC/component cleanup could run during a pending servicing restart, and SFC continued after DISM failure/restart. Those paths now defer servicing. DISM repair/cleanup explicitly uses `/NoRestart`; deep cleanup releases stay-awake in `finally` and retains a failed ResetBase result. +- DNS flush and performance-counter WMI resync could fail while reporting success. Native exit codes now affect their results, and failure of a requested TCP/IP/firewall reset remains a warning after DNS recovery. Cache cleanup no longer references deleted temp counters, and thumbnail counts reflect successful removals. +- Store repair enumerated every user's AppX packages, then registered them into the current account. Registration now uses only the current user's packages and errors when enumeration fails. +- An unavailable event log could appear clean. The scan and HTML report now share checked event reading; failures remain unavailable/warnings instead of zero-error verdicts. Disabled/unreadable services remain warnings in the service-health summary. +- A failed network time resync automatically unregistered W32Time, losing existing time-source configuration. Failed resync now reports the native cause and preserves configuration. +- Icon rebuild stopped Explorer across all sessions. It now stops/restarts Explorer only in the launching session. Windows-drive diagnostics use `SystemDrive` rather than assuming `C:`. +- Service cleanup treated unreadable/offline/remote paths as missing programs. It now requires a stopped service, a confirmed missing executable on a ready local fixed disk, a fresh state/path check and a nonempty registration backup before deletion. +- AI-policy restore could reject signed DWORD/QWORD values or mishandle singleton Binary/MultiString values; case-insensitive JSON equality could hide an incorrect restoration. Types now round-trip exactly, and snapshot destinations/types are validated before writes. +- Gaming snapshots read and later overwrote unrelated boot/network/service/power state even for a HAGS or mouse change. Schema 3 records originating identity and limits capture to action scope. UTF-8 pointers preserve Unicode paths. Registry targets/types and service/boot/power states are validated before restore; actual typed values, service state/start mode, NIC settings and native outcomes are checked. +- Gaming restore/legacy cleanup suppressed native, registry and service failures while claiming exact success. Failures now propagate. BCD query failure cannot become an all-absent snapshot, and cleanup deletes only present overrides with read-back checks. +- Console SSD ReTrim also targeted HDD/unknown volumes. It now requires a unique SSD serial-number match and supported fixed volumes. +- Shader cleanup followed a linked cache root and could delete unrelated target files. Roots/ancestors and nested entries are checked for reparse points, and linked items are skipped. +- Three batch launchers embedded paths in apostrophe-sensitive PowerShell source, used the Server service as an administrator test, and gaming launchers relied on a timed elevation flag. Paths now cross through environment variables, administrator status uses WindowsPrincipal, and elevation waits for the actual child exit code. Elevation passes the originating user SID; the repair/gaming/VR entry points reject alternate-account elevation before bootstrap and per-user changes. +- Both consoles now provide bounded versioned JSONL diagnostics in LocalAppData. Default logs contain warning/error causes; `-DebugLog` / `WINDOWS_PC_TOOLKIT_DEBUG=1` adds startup/configuration/state detail. Fixer transcripts are optional and retained separately from recovery backups. + +## Verification + +Run with built-in Windows PowerShell 5.1: + +```powershell +powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\tests\Test_AuditRepairGaming.ps1 +``` + +The regression suite passes **49 checks**. It uses real temporary HKCU registry values, JSON serialization, Unicode/apostrophe paths, ordinary cache files, a real directory junction, generated HTML, actual isolated user-identity guards, and a harmless compiled executable to verify Win32 argument quoting and failed native BCD-query handling. Service, storage, AppX, time, event-log and servicing mutations are injected fixtures; no repair or gaming setting is applied to the test host. Existing project integration tests are separate. + +## Limits + +These checks establish the repaired control flow, state boundaries and native argument handling. They do not certify repair of a corrupt Windows installation, a successful Windows Update, hardware-specific HAGS/VR behavior, AppX re-registration on every build, or FPS improvement. Old gaming snapshots lack computer/user metadata and require explicit review; console restore can replace later manual edits. Use dashboard journals for conflict-aware recovery. ResetBase, deleted shader data and external Windows servicing effects are not reversible by a registry snapshot. + +Primary contracts checked: [Add-AppxPackage user scope](https://learn.microsoft.com/powershell/module/appx/add-appxpackage), [SetThreadExecutionState result/flags](https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate), and the actual Windows PowerShell 5.1 parser/runtime and installed native tool interfaces. diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png index 3e2f97c00a39274f95747c2b9c01bb8e8ab60fcb..9b663d2aa1b3a46faae41e89ab53f3739cce6183 100644 GIT binary patch literal 115248 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 literal 115431 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%0DH=fJlER7qBf+ay^|souJM`)UI_wYhB3hX3wb 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�-*2;lJ)S(B(5 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

+Ub^^>;o23twvbHQ5F2lVta@C?{Ae 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 diff --git a/docs/specs/2026-09-18-pc-cleaner-design.md b/docs/specs/2026-09-18-pc-cleaner-design.md index ba10499..948aac1 100644 --- a/docs/specs/2026-09-18-pc-cleaner-design.md +++ b/docs/specs/2026-09-18-pc-cleaner-design.md @@ -1,5 +1,7 @@ # PC Cleaner v1.0.0 — design spec (2026-09-18) +Historical proposal, not the current implementation contract. Use the [current cleaner README](../../pc-cleaner/README.md) and [2026-10-10 audit](../audit-cleaner.md) for behavior and limits. Windows.old, backups, Recall and update downloads are report-only in the current tool. + ## Goal A fifth toolkit tool that beats CCleaner (adware), BleachBit (unclear labels, no registry cleaner, thin Windows coverage) and Razer Cortex (bloat) at their own game: **scan-first, plainly labeled, honest disk cleanup + registry care** that never touches user data. diff --git a/pc-cleaner/PC_Cleaner.ps1 b/pc-cleaner/PC_Cleaner.ps1 index 320435a..bb28946 100644 --- a/pc-cleaner/PC_Cleaner.ps1 +++ b/pc-cleaner/PC_Cleaner.ps1 @@ -1,7 +1,7 @@ #requires -version 5.1 <# .SYNOPSIS - PC Cleaner v1.1 - scan-first disk cleanup and registry care for Windows 10/11. + PC Cleaner v1.1.1 - scan-first disk cleanup and registry care for Windows 10/11. .DESCRIPTION Every category is labeled with what it is and why it is safe. Nothing is deleted without an explicit profile/pick and a confirmation. User data - @@ -14,16 +14,32 @@ #> param( [ValidateSet('Menu','Scan','SelfTest','Library')] - [string]$Action = 'Menu' + [string]$Action = 'Menu', + [switch]$DebugLog, + [string]$ExpectedUserSid ) $ErrorActionPreference = 'Stop' -$Script:Version = '1.1' +if ($ExpectedUserSid -and [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -ne $ExpectedUserSid) { throw 'Run Cleaner as an administrator for your own Windows account; a different account would clean and change the wrong user profile.' } +$Script:Version = '1.1.1' +$script:CleanerDebugEnabled = ($Action -ne 'Library' -and ($DebugLog -or $env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1')) $StateRoot = Join-Path $env:ProgramData 'WindowsPCToolkit\Cleaner' $LogRoot = Join-Path $StateRoot 'Logs' $RegBackupRoot = Join-Path $StateRoot 'RegistryBackups' $FileBackupRoot = Join-Path $StateRoot 'FileBackups' +function Write-CleanerDiagnostic { + param([string]$Event,[string]$Message) + if (-not $script:CleanerDebugEnabled) { return } + try { + $folder=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs' + [void][IO.Directory]::CreateDirectory($folder) + $log=Join-Path $folder 'Cleaner_debug.log' + if ((Test-Path -LiteralPath $log) -and (Get-Item -LiteralPath $log).Length -ge 1MB) { Move-Item -LiteralPath $log -Destination ($log+'.1') -Force } + Add-Content -LiteralPath $log -Encoding UTF8 -Value ("{0} v{1} {2}: {3}" -f (Get-Date).ToString('o'),$Script:Version,$Event,$Message) + } catch { Write-Warning ("Could not write cleaner debug log: "+$_.Exception.Message) } +} + # ============================================================================ # UI + BASIC HELPERS # ============================================================================ @@ -42,6 +58,7 @@ function Write-Status { Step = @{ Tag=' [=>] '; Color='Cyan' } } $m = $map[$Type] + Write-CleanerDiagnostic $Type $Text Write-Host $m.Tag -ForegroundColor $m.Color -NoNewline Write-Host $Text -ForegroundColor $(if ($Type -eq 'Info') { 'Gray' } else { 'White' }) } @@ -75,17 +92,43 @@ $Script:ProtectedNames = @( 'Preferences','Secure Preferences','Local State','Extension State','Extensions','WebCache', # user folders 'Desktop','Documents','Pictures','Videos','Music','Favorites','Contacts','Saved Games', - 'OneDrive','Dropbox','Google Drive' + 'OneDrive','Dropbox','Google Drive', + '.ollama','.lmstudio','huggingface','torch','whisper','.llama.cpp' ) function Test-IsProtectedPath { param([Parameter(Mandatory)][string]$Path) + try { $Path=[IO.Path]::GetFullPath($Path) } catch { return $true } foreach ($segment in ($Path -split '[\\/]')) { if ($Script:ProtectedNames -contains $segment) { return $true } } return $false } +function Test-CleanPathWithoutLinks { + param([Parameter(Mandatory)][string]$Path) + try { + $current=[IO.Path]::GetFullPath($Path) + while ($current) { + if ((Get-Item -LiteralPath $current -Force -ErrorAction Stop).Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false } + $current=Split-Path $current -Parent + } + return $true + } catch { return $false } +} + +function Get-CleanTreeFiles { + param([string]$Path,[switch]$Report) + if (-not (Test-CleanPathWithoutLinks $Path) -or (-not $Report -and (Test-IsProtectedPath $Path))) { return } + $stack=New-Object Collections.Stack; $stack.Push($Path) + while ($stack.Count) { + foreach ($item in Get-ChildItem -LiteralPath $stack.Pop() -Force -ErrorAction SilentlyContinue) { + if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (-not $Report -and (Test-IsProtectedPath $item.FullName))) { continue } + if ($item.PSIsContainer) { $stack.Push($item.FullName) } else { $item } + } + } +} + function Assert-NotProtected { param([Parameter(Mandatory)][string]$Path) if (Test-IsProtectedPath -Path $Path) { @@ -203,8 +246,8 @@ function Get-CleanCategories { Risk='KEEP'; Profile='Report'; Kind='Report'; Paths=@("$L\CoreAIPlatform.00"); Desc='Your searchable Recall screen history. Deleting this erases that history permanently.' } [pscustomobject]@{ Id='WindowsOld'; Section='System'; Label='Previous Windows installation (Windows.old)'; - Risk='STRICT'; Profile='Strict'; Kind='Special'; Paths=@(); - Desc='Left by a Windows upgrade. Removing frees many GB; rollback to the previous build becomes impossible.' } + Risk='KEEP'; Profile='Report'; Kind='Report'; Paths=@("$env:SystemDrive\Windows.old"); + Desc='Previous Windows installation, kept for rollback. Use Windows Storage settings to review removal.' } ) } @@ -229,7 +272,7 @@ function Resolve-CategoryDirs { $out = @() foreach ($p in $Category.Paths) { if ($p -match '\*') { - $out += @(Get-Item -Path $p -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer }) + $out += @(Get-Item -Path ([WildcardPattern]::Escape($p).Replace('`*','*')) -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer }) } elseif (Test-Path -LiteralPath $p -PathType Container -ErrorAction SilentlyContinue) { $out += Get-Item -LiteralPath $p -Force } @@ -242,7 +285,7 @@ function Resolve-CategoryFiles { $out = @() foreach ($p in $Category.Paths) { if ($p -match '\*') { - $out += @(Get-ChildItem -Path $p -Force -File -ErrorAction SilentlyContinue) + $out += @(Get-ChildItem -Path ([WildcardPattern]::Escape($p).Replace('`*','*')) -Force -File -ErrorAction SilentlyContinue) } elseif (Test-Path -LiteralPath $p -PathType Leaf -ErrorAction SilentlyContinue) { $out += Get-Item -LiteralPath $p -Force } @@ -256,17 +299,19 @@ function Measure-Category { switch ($Category.Kind) { 'Children' { foreach ($dir in Resolve-CategoryDirs -Category $Category) { - $items = @(Get-ChildItem -LiteralPath $dir.FullName -Recurse -File -Force -ErrorAction SilentlyContinue) + $items = if ($Category.Id -in @('UserTemp','WindowsTemp')) { @(Get-DisposableTempFiles @($dir.FullName)) } else { @(Get-CleanTreeFiles $dir.FullName) } $files += $items.Count $bytes += [long](($items | Measure-Object -Property Length -Sum).Sum) } } 'Files' { - foreach ($f in Resolve-CategoryFiles -Category $Category) { $files++; $bytes += $f.Length } + foreach ($f in Resolve-CategoryFiles -Category $Category) { + if ((Test-CleanPathWithoutLinks $f.FullName) -and -not (Test-IsProtectedPath $f.FullName)) { $files++; $bytes += $f.Length } + } } 'Report' { foreach ($dir in Resolve-CategoryDirs -Category $Category) { - $items = @(Get-ChildItem -LiteralPath $dir.FullName -Recurse -File -Force -ErrorAction SilentlyContinue) + $items = @(Get-CleanTreeFiles $dir.FullName -Report) $files += $items.Count $bytes += [long](($items | Measure-Object -Property Length -Sum).Sum) } @@ -276,23 +321,15 @@ function Measure-Category { 'DeliveryOptimization' { $p = Join-Path $env:SystemRoot 'ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization' if (Test-Path -LiteralPath $p -ErrorAction SilentlyContinue) { - $items = @(Get-ChildItem -LiteralPath $p -Recurse -File -Force -ErrorAction SilentlyContinue) + $items = @(Get-CleanTreeFiles $p) $files += $items.Count $bytes += [long](($items | Measure-Object -Property Length -Sum).Sum) } } 'BakFiles' { $r = Get-BakCandidates - $files += @($r.Deletable).Count - foreach ($f in $r.Deletable) { $bytes += $f.Length } - } - 'WindowsOld' { - $p = Join-Path $env:SystemDrive 'Windows.old' - if (Test-Path -LiteralPath $p -ErrorAction SilentlyContinue) { - $items = @(Get-ChildItem -LiteralPath $p -Recurse -File -Force -ErrorAction SilentlyContinue) - $files += $items.Count - $bytes += [long](($items | Measure-Object -Property Length -Sum).Sum) - } + $files += @($r.Deletable).Count + @($r.Orphans).Count + foreach ($f in @($r.Deletable)+@($r.Orphans)) { $bytes += $f.Length } } } } @@ -306,7 +343,7 @@ function Remove-ItemSafe { if (Test-IsProtectedPath -Path $Path) { return $false } try { $item=Get-Item -LiteralPath $Path -Force -ErrorAction Stop - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false } + if (-not (Test-CleanPathWithoutLinks $item.FullName)) { return $false } if ($Directory) { $stack=New-Object Collections.Stack; $stack.Push($Path) while ($stack.Count) { @@ -319,7 +356,7 @@ function Remove-ItemSafe { if ($Directory) { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop } else { Remove-Item -LiteralPath $Path -Force -ErrorAction Stop } return $true - } catch { return $false } + } catch { Write-CleanerDiagnostic 'RemovalFailure' ("$Path : "+$_.Exception.Message); return $false } } function Get-BakCandidates { @@ -332,7 +369,7 @@ function Get-BakCandidates { $deletable = New-Object System.Collections.ArrayList $orphans = New-Object System.Collections.ArrayList foreach ($root in $Roots) { - if (-not $root -or -not (Test-Path -LiteralPath $root -PathType Container -ErrorAction SilentlyContinue)) { continue } + if (-not $root -or -not (Test-Path -LiteralPath $root -PathType Container -ErrorAction SilentlyContinue) -or -not (Test-CleanPathWithoutLinks $root)) { continue } $stack = New-Object System.Collections.Stack $stack.Push([pscustomobject]@{ Path = $root; Depth = 0 }) while ($stack.Count -gt 0) { @@ -344,6 +381,7 @@ function Get-BakCandidates { $stack.Push([pscustomobject]@{ Path = $d.FullName; Depth = $cur.Depth + 1 }) } foreach ($f in @(Get-ChildItem -LiteralPath $cur.Path -File -Force -ErrorAction SilentlyContinue)) { + if (($f.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Test-IsProtectedPath $f.FullName)) { continue } if ($exts -notcontains $f.Extension.ToLowerInvariant()) { continue } $original = Join-Path $cur.Path ([IO.Path]::GetFileNameWithoutExtension($f.Name)) if (Test-Path -LiteralPath $original -PathType Leaf -ErrorAction SilentlyContinue) { [void]$deletable.Add($f) } @@ -356,13 +394,16 @@ function Get-BakCandidates { function Invoke-CleanCategory { param($Category) - $result = [pscustomobject]@{ Id = $Category.Id; Label = $Category.Label; Removed = 0; Skipped = 0; Bytes = [long]0; Note = '' } - if ($Category.Profile -eq 'Report' -or $Category.Kind -eq 'Report') { $result.Note='Review only; kept all files.'; return $result } + $result = [pscustomobject]@{ Id = $Category.Id; Label = $Category.Label; Removed = 0; Skipped = 0; Bytes = [long]0; Note = ''; Status='Completed' } + if ($Category.Profile -eq 'Report' -or $Category.Kind -eq 'Report' -or $Category.Id -in @('WindowsOld','BakFiles','WUCache','RecallData','AiAppCaches','ModelStores')) { $result.Status='Skipped'; $result.Note='Review only; kept all files.'; return $result } switch ($Category.Kind) { 'Children' { foreach ($dir in Resolve-CategoryDirs -Category $Category) { + if ((Test-IsProtectedPath $dir.FullName) -or -not (Test-CleanPathWithoutLinks $dir.FullName)) { $result.Skipped++; continue } if ($Category.Id -in @('UserTemp','WindowsTemp')) { foreach ($file in Get-DisposableTempFiles -Roots @($dir.FullName)) { + $current=Get-Item -LiteralPath $file.FullName -Force -ErrorAction SilentlyContinue + if (-not $current -or $current.LastWriteTime -ge (Get-Date).AddDays(-7) -or $current.LastAccessTime -ge (Get-Date).AddDays(-7)) { $result.Skipped++; continue } if (Remove-ItemSafe $file.FullName) { $result.Removed++; $result.Bytes+=$file.Length } else { $result.Skipped++ } } @@ -371,7 +412,7 @@ function Invoke-CleanCategory { foreach ($child in @(Get-ChildItem -LiteralPath $dir.FullName -Force -ErrorAction SilentlyContinue)) { if ($child.Attributes -band [IO.FileAttributes]::ReparsePoint) { $result.Skipped++; continue } $size = if ($child.PSIsContainer) { - [long](@(Get-ChildItem -LiteralPath $child.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum) + [long](@(Get-CleanTreeFiles $child.FullName | Measure-Object -Property Length -Sum).Sum) } else { [long]$child.Length } if (Remove-ItemSafe -Path $child.FullName -Directory:$child.PSIsContainer) { $result.Removed++ @@ -393,28 +434,11 @@ function Invoke-CleanCategory { try { $before = Measure-Category -Category $Category Delete-DeliveryOptimizationCache -Force -ErrorAction Stop - $result.Removed = $before.Files; $result.Bytes = $before.Bytes - } catch { $result.Note = $_.Exception.Message } - } else { $result.Note = 'cmdlet not available on this build' } - } - 'BakFiles' { - $r = Get-BakCandidates - foreach ($f in $r.Deletable) { - if (Remove-ItemSafe -Path $f.FullName) { $result.Removed++; $result.Bytes += [long]$f.Length } - else { $result.Skipped++ } - } - $result.Note = "$(@($r.Orphans).Count) backup file(s) kept (no original found)" - } - 'WindowsOld' { - $p = Join-Path $env:SystemDrive 'Windows.old' - if (Test-Path -LiteralPath $p -ErrorAction SilentlyContinue) { - Write-Status Info 'Taking ownership of Windows.old (this can take several minutes)...' - & "$env:SystemRoot\System32\takeown.exe" /f $p /r /d y 2>&1 | Out-Null - & "$env:SystemRoot\System32\icacls.exe" $p /grant '*S-1-5-32-544:(OI)(CI)F' /t /c 2>&1 | Out-Null - $before = Measure-Category -Category $Category - if (Remove-ItemSafe -Path $p -Directory) { $result.Removed = $before.Files; $result.Bytes = $before.Bytes } - else { $result.Note = 'removal incomplete - some files are still protected' } - } + $after = Measure-Category -Category $Category + $result.Removed = [math]::Max(0,$before.Files-$after.Files); $result.Bytes = [math]::Max(0,$before.Bytes-$after.Bytes) + $result.Note = 'Measured cache reduction after Windows cleanup; pinned/in-use files may remain.' + } catch { $result.Status='Failed'; $result.Note = $_.Exception.Message; Write-CleanerDiagnostic 'CleanupFailure' $result.Note } + } else { $result.Status='Skipped'; $result.Note = 'cmdlet not available on this build' } } } } @@ -436,6 +460,8 @@ function Get-CommandTargetPath { } else { $m = [regex]::Match($c, '^(.*?\.exe)(\s|$)', 'IgnoreCase') if ($m.Success) { $c = $m.Groups[1].Value } else { $c = ($c -split '\s+')[0] } + # Unquoted paths with spaces have several CreateProcess candidates. + if ($c -match '\s' -or $CommandLine -match '^\s*[^"\s]+\.exe\s+[^-"/]*\\') { return $null } } if (-not [IO.Path]::IsPathRooted($c)) { return $null } return $c @@ -447,7 +473,7 @@ function Get-DisposableTempFiles { foreach ($root in $Roots) { if (-not (Test-Path -LiteralPath $root -PathType Container)) { continue } $rootItem=Get-Item -LiteralPath $root -Force -ErrorAction Stop - if ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } + if ((Test-IsProtectedPath $rootItem.FullName) -or -not (Test-CleanPathWithoutLinks $rootItem.FullName)) { continue } $stack=New-Object Collections.Stack; $stack.Push($rootItem.FullName) while ($stack.Count) { foreach ($item in Get-ChildItem -LiteralPath $stack.Pop() -Force -ErrorAction SilentlyContinue) { @@ -494,7 +520,7 @@ function Get-RegistryIssues { try { $props = Get-ItemProperty -LiteralPath $key -ErrorAction Stop foreach ($prop in $props.PSObject.Properties) { - if ($prop.Name -match '^PS') { continue } + if ($prop.Name -in @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider')) { continue } $target = Get-CommandTargetPath ([string]$prop.Value) if ($target -and -not (Test-Path -LiteralPath $target -ErrorAction SilentlyContinue)) { [void]$issues.Add([pscustomobject]@{ @@ -532,7 +558,7 @@ function Get-RegistryIssues { try { $props = Get-ItemProperty -LiteralPath $mui -ErrorAction Stop foreach ($prop in $props.PSObject.Properties) { - if ($prop.Name -match '^PS') { continue } + if ($prop.Name -in @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider')) { continue } $pathPart = $prop.Name -replace '\.(FriendlyAppName|ApplicationCompany|NoRemove)$', '' if ($pathPart -notmatch '\\') { continue } $pathPart = [Environment]::ExpandEnvironmentVariables($pathPart) @@ -593,15 +619,26 @@ function Invoke-RegistryCare { try { $run=New-SuiteRun 'RegistryCare' foreach ($issue in $selected) { + $start=$run.Registry.Count try { - $target=if ($issue.Category -eq 'Dead startup entries') { Get-CommandTargetPath $issue.Display } else { $issue.Display } + $target=$issue.Display + if ($issue.Category -eq 'Dead startup entries') { + $current=Get-Item -LiteralPath $issue.Key -ErrorAction Stop + $target=if ($current.GetValueNames() -contains $issue.Name) { Get-CommandTargetPath ([string]$current.GetValue($issue.Name,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)) } else { $null } + } if ($issue.Kind -ne 'Value' -or $issue.Category -notin @('Dead startup entries','Stale MuiCache entries') -or -not (Test-SuiteMissingExecutable $target)) { Add-SuiteStep $run $issue.Display 'Skipped' 'Review candidate retained. No registry key/app registration/shortcut removed.' continue } Set-SuiteRegistry $run $issue.Key $issue.Name $null -Remove Add-SuiteStep $run $issue.Name 'Applied' 'Dead value removed with exact typed-value backup.' - } catch { Add-SuiteStep $run $issue.Name 'Failed' $_.Exception.Message } + } catch { + $message=$_.Exception.Message + for ($index=$run.Registry.Count-1; $index -ge $start; $index--) { + try { Undo-SuiteRegistryEntry $run.Registry[$index] } catch { $message+=' Rollback: '+$_.Exception.Message } + } + Add-SuiteStep $run $issue.Name 'Failed' $message + } } $run.Status=if (@($run.Steps | Where-Object Status -eq 'Failed').Count) { 'CompletedWithErrors' } else { 'Completed' } Save-SuiteJson $run.Path $run @@ -621,10 +658,15 @@ function Restore-LatestRegistryBackup { if (-not (Test-Path -LiteralPath $manifestPath -ErrorAction SilentlyContinue)) { Write-Status Warn 'Latest backup has no manifest.'; return } $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json Write-Status Info ("Restoring backup from {0} ({1} item(s))..." -f $latest.Name, @($manifest.Items).Count) + if (@($manifest.Items | Where-Object Type -ne 'File').Count) { + Write-Status Warn 'Legacy registry snapshots import whole keys and can replace changes made after backup.' + if ((Read-Host ' Import these legacy registry snapshots? (y/N)') -notmatch '^[Yy]$') { Write-Status Skip 'Legacy backup retained; nothing restored.'; return } + } $restored = 0 foreach ($item in @($manifest.Items)) { if ($item.Type -eq 'File') { try { + if (Test-Path -LiteralPath $item.Path) { Write-Status Skip ("Existing file kept: "+$item.Path); continue } $dest = Split-Path $item.Path -Parent if (-not (Test-Path -LiteralPath $dest -ErrorAction SilentlyContinue)) { New-Item -ItemType Directory -Path $dest -Force | Out-Null } Copy-Item -LiteralPath $item.Backup -Destination $item.Path -Force -ErrorAction Stop @@ -650,7 +692,7 @@ function Show-ScanLine { param($Category, $Info, [string]$Prefix = '') $tag = switch ($Category.Risk) { 'SAFE' { '[SAFE] ' } 'CAREFUL' { '[CAREFUL]' } - 'STRICT' { '[STRICT] ' } 'REPORT' { '[KEEP] ' } + 'STRICT' { '[STRICT] ' } 'REPORT' { '[KEEP] ' } 'KEEP' { '[KEEP] ' } default { '[----] ' } } $color = switch ($Category.Risk) { 'SAFE' { 'Green' } 'CAREFUL' { 'Yellow' } 'STRICT' { 'Red' } default { 'DarkCyan' } } @@ -661,7 +703,7 @@ function Show-ScanLine { function Show-ScanReport { $cats = Get-CleanCategories - $rep = Get-ReportCategories + $rep = @(Get-ReportCategories) + @($cats | Where-Object Profile -eq 'Report') Write-Host '' Write-Status Step 'Scanning (read-only - nothing is deleted by a scan)...' $measured = @{} @@ -708,12 +750,13 @@ function Get-FreeSpaceBytes { function Save-RunLog { param([string]$Name, $Results, [long]$FreeBefore, [long]$FreeAfter) Initialize-State - $path = Join-Path $LogRoot ("clean_{0}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss')) + $path = Join-Path $LogRoot ("clean_{0}_{1}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss'),[guid]::NewGuid().ToString('N')) $obj = [pscustomobject]@{ Tool = 'PC Cleaner'; Version = $Script:Version; Profile = $Name Created = (Get-Date).ToString('o'); Computer = $env:COMPUTERNAME FreeBeforeBytes = $FreeBefore; FreeAfterBytes = $FreeAfter TotalRemovedBytes = [long](($Results | Measure-Object -Property Bytes -Sum).Sum) + Status = $(if (@($Results | Where-Object Status -eq 'Failed').Count) { 'CompletedWithErrors' } else { 'Completed' }) Categories = @($Results) } ($obj | ConvertTo-Json -Depth 6) | Set-Content -LiteralPath $path -Encoding UTF8 @@ -735,7 +778,7 @@ function Invoke-Profile { foreach ($c in $cats) { $r = Invoke-CleanCategory -Category $c [void]$results.Add($r) - Write-Status OK ("{0}: removed {1} item(s), freed {2}{3}" -f $c.Label, $r.Removed, (Format-Size $r.Bytes), + Write-Status $(if ($r.Status -eq 'Failed') { 'Fail' } elseif ($r.Status -eq 'Skipped') { 'Skip' } else { 'OK' }) ("{0}: removed {1} item(s), freed {2}{3}" -f $c.Label, $r.Removed, (Format-Size $r.Bytes), $(if ($r.Skipped -gt 0) { " ($($r.Skipped) locked/in-use skipped)" } else { '' })) if ($r.Note) { Write-Status Info (" {0}" -f $r.Note) } } @@ -746,7 +789,7 @@ function Invoke-Profile { $totalSkipped = [int](($results | Measure-Object -Property Skipped -Sum).Sum) Write-Host '' Write-Host (" ============================================================") -ForegroundColor DarkCyan - Write-Status OK ("{0} clean complete: {1} item(s) removed, {2} skipped, {3} freed." -f $Name, $totalRemoved, $totalSkipped, (Format-Size $totalBytes)) + Write-Status $(if (@($results | Where-Object Status -eq 'Failed').Count) { 'Warn' } else { 'OK' }) ("{0} clean complete: {1} item(s) removed, {2} skipped, {3} freed. {4} category failure(s)." -f $Name, $totalRemoved, $totalSkipped, (Format-Size $totalBytes),@($results | Where-Object Status -eq 'Failed').Count) Write-Status Info ("Disk free: {0} -> {1}" -f (Format-Size $freeBefore), (Format-Size $freeAfter)) Write-Status Info ("Log: {0}" -f $log) } @@ -763,8 +806,12 @@ function Invoke-PickCategories { if ($pick -eq '0' -or [string]::IsNullOrWhiteSpace($pick)) { return } $selected = @() foreach ($part in ($pick -split ',')) { - if ($part -match '^\s*(\d+)\s*-\s*(\d+)\s*$') { foreach ($n in ([int]$Matches[1])..([int]$Matches[2])) { if ($n -ge 1 -and $n -le $cats.Count) { $selected += $cats[$n - 1] } } } - elseif ($part -match '^\s*(\d+)\s*$') { $idx = [int]$Matches[1]; if ($idx -ge 1 -and $idx -le $cats.Count) { $selected += $cats[$idx - 1] } } + $first=0; $last=0 + if ($part -match '^\s*(\d+)\s*-\s*(\d+)\s*$') { + if ([int]::TryParse($Matches[1],[ref]$first) -and [int]::TryParse($Matches[2],[ref]$last) -and $first -ge 1 -and $first -le $cats.Count -and $last -ge 1 -and $last -le $cats.Count) { + foreach ($n in $first..$last) { $selected += $cats[$n-1] } + } + } elseif ([int]::TryParse($part.Trim(),[ref]$first) -and $first -ge 1 -and $first -le $cats.Count) { $selected += $cats[$first-1] } } $selected = @($selected | Where-Object { $_ } | Select-Object -Unique) if (@($selected).Count -eq 0) { Write-Status Skip 'Nothing valid selected.'; return } @@ -776,11 +823,12 @@ function Invoke-PickCategories { foreach ($c in $selected) { $r = Invoke-CleanCategory -Category $c [void]$results.Add($r) - Write-Status OK ("{0}: removed {1} item(s), freed {2}{3}" -f $c.Label, $r.Removed, (Format-Size $r.Bytes), + Write-Status $(if ($r.Status -eq 'Failed') { 'Fail' } elseif ($r.Status -eq 'Skipped') { 'Skip' } else { 'OK' }) ("{0}: removed {1} item(s), freed {2}{3}" -f $c.Label, $r.Removed, (Format-Size $r.Bytes), $(if ($r.Skipped -gt 0) { " ($($r.Skipped) locked/in-use skipped)" } else { '' })) + if ($r.Note) { Write-Status Info $r.Note } } $log = Save-RunLog -Name 'Custom' -Results $results -FreeBefore $freeBefore -FreeAfter (Get-FreeSpaceBytes) - Write-Status OK ("Custom clean complete. Log: {0}" -f $log) + Write-Status $(if (@($results | Where-Object Status -eq 'Failed').Count) { 'Warn' } else { 'OK' }) ("Custom clean complete. Log: {0}" -f $log) } function Invoke-RecycleBin { @@ -861,6 +909,7 @@ function Show-Menu { } if ($Action -eq 'Library') { return } +Write-CleanerDiagnostic 'Startup' ("Action=$Action; StateRoot=$StateRoot; LogRoot=$LogRoot; Temp=$env:TEMP") switch ($Action) { 'SelfTest' { Invoke-SelfTest } 'Scan' { Initialize-State; Show-ScanReport | Out-Null; exit 0 } @@ -881,7 +930,7 @@ switch ($Action) { '0' { exit 0 } default { Write-Status Warn 'Invalid selection.' } } - } catch { Write-Status Fail $_.Exception.Message } + } catch { Write-CleanerDiagnostic 'Exception' ($_.Exception.ToString()+"`n"+$_.ScriptStackTrace); Write-Status Fail $_.Exception.Message } } } } diff --git a/pc-cleaner/README.md b/pc-cleaner/README.md index 7f662a2..a3dd586 100644 --- a/pc-cleaner/README.md +++ b/pc-cleaner/README.md @@ -1,26 +1,35 @@ -# PC Cleaner 1.1 +# PC Cleaner 1.1.1 Scan-first cleanup and targeted registry repair. Use the [dashboard](../README.md) or `Run_As_Admin.bat` for the detailed console. Extract the complete toolkit; registry care uses shared root `Modules`. +Elevation preserves the originating Windows account identity. If Windows asks for a different administrator account, the cleaner refuses to act on that account's files or HKCU settings. + | Profile | Categories | | --- | --- | | Balanced | Aged user/Windows Temp, thumbnail/icon caches, error-report queues, WinINet/browser page caches, editor caches and official Delivery Optimization cleanup. | | Strict, selected and confirmed | Shaders, crash dumps, servicing logs, Prefetch and extra browser caches. Recompiling shaders can cause stutter after cleanup. | | Report only | AI model/app stores, Recall data, patcher backups/leftovers, Windows Update download folders and Windows.old. The category executor cannot delete them. Use Windows storage/servicing tools for update/rollback data. | -Protected names include cookies, logins, history, bookmarks, sessions, app storage, editor history/extensions and personal folders. Temp cleanup keeps recent/recently accessed files, skips links and does not kill apps. Counts reflect successful removal; deleted files are not undoable. +Protected names include cookies, logins, history, bookmarks, sessions, app storage, editor history/extensions, personal folders and common local model stores. Cleanup checks links in every parent directory as well as the target, so a browser profile or cache below a junction is kept. Temp scan totals include only files older than seven days for both writing and access; cleanup checks the age again before deletion. Deleted files are not undoable. + +All report-only categories appear in the scan. Windows.old, patcher backups, AI stores, Recall data and Windows Update downloads cannot be deleted by any cleaner profile or an older category descriptor. Delivery Optimization reports the measured before/after reduction rather than claiming all cached files were removed; command failures remain failures in the run report. ## Registry care -Missing targets are listed for review. Only dead Run/RunOnce and MuiCache values with executables confirmed absent on a ready fixed local drive are eligible for removal. Offline/removable/network targets, permission failures, uninstall/App Paths registrations and shortcuts are kept. +Missing targets are listed for review. Only dead Run/RunOnce and MuiCache values with executables confirmed absent on a ready fixed local drive are eligible for removal. The startup command is read again immediately before repair. Offline/removable/network targets, ambiguous unquoted paths, permission failures, uninstall/App Paths registrations and shortcuts are kept. -Numbered choices use one consistent order. Each removal saves exact value/type to the Suite journal before writing. Menu **7** restores the latest registry-care operation, checks the result and preserves intervening changes. Legacy `.reg` backups remain available when no new registry-care journal exists. +Numbered choices use one consistent order. Each removal saves exact value/type to the Suite journal before writing. A failed write attempts to restore that value. Menu **7** restores the latest registry-care operation, checks the result and preserves intervening changes. Legacy `.reg` backups remain available when no new registry-care journal exists; importing whole legacy keys requires confirmation, and existing shortcut files are preserved. No broad sweep, COM deletion, registry defrag or FPS claim from unused keys. ```powershell .\PC_Cleaner.ps1 -Action Scan .\PC_Cleaner.ps1 -Action SelfTest +.\PC_Cleaner.ps1 -DebugLog ``` Logs: `%ProgramData%\WindowsPCToolkit\Cleaner\Logs`. New registry recovery: `WindowsPCToolkit\Suite\Runs`. Launch errors: `%LOCALAPPDATA%\Temp\PCCleaner_launch.log`. + +Debug logging is off by default. `-DebugLog` or `WINDOWS_PC_TOOLKIT_DEBUG=1` records the version, resolved paths, state changes and failures in `%LOCALAPPDATA%\WindowsPCToolkit\Logs\Cleaner_debug.log`, rotating at 1 MiB. Importing the cleaner as a library stays quiet. Run reports use unique names so rapid repeated runs keep their own evidence. + +Audit regression checks: `powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File ..\tests\Test_AuditCleaner.ps1`. diff --git a/pc-cleaner/Run_As_Admin.bat b/pc-cleaner/Run_As_Admin.bat index 7e7e7c8..b6bc98f 100644 --- a/pc-cleaner/Run_As_Admin.bat +++ b/pc-cleaner/Run_As_Admin.bat @@ -1,30 +1,23 @@ @echo off -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\PCCleaner_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\PCCleaner_elevated.flag" +set "PCCLEANER_LAUNCH=%~f0" -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click Run_As_Admin.bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }; exit 1" +if errorlevel 1 goto :elevate echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" -"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Cleaner.ps1" +"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Cleaner.ps1" -ExpectedUserSid "%~1" +set "RC=%ERRORLEVEL%" +goto :finish + +:elevate +echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:PCCLEANER_LAUNCH -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 set "RC=%ERRORLEVEL%" + +:finish echo [%date% %time%] PC_Cleaner.ps1 exited with %RC% >> "%LOG%" if not "%RC%"=="0" ( echo. diff --git a/pc-corruption-fixer/Fix_Corruption.bat b/pc-corruption-fixer/Fix_Corruption.bat index ace9869..5132574 100644 --- a/pc-corruption-fixer/Fix_Corruption.bat +++ b/pc-corruption-fixer/Fix_Corruption.bat @@ -1,19 +1,21 @@ @echo off -title PC Corruption Fixer v7.2 -color 0B - -:: SECURITY: use the absolute path to powershell.exe so a rogue -:: powershell.exe next to this script (or earlier in PATH) is never used. -set "PS_EXE=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" - -:: Self-elevate to Administrator, then launch the PowerShell script -net session >nul 2>&1 -if errorlevel 1 ( - echo Requesting Administrator privileges... - "%PS_EXE%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" - exit /b -) - -"%PS_EXE%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Fixer.ps1" +title PC Corruption Fixer v7.2.1 +setlocal EnableExtensions DisableDelayedExpansion +set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" +set "LOG=%LOCALAPPDATA%\Temp\PCFixer_launch.log" +set "TOOLKIT_LAUNCHER=%~f0" +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }; exit 1" +if errorlevel 1 goto :elevate +"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Fixer.ps1" -ExpectedUserSid "%~1" +set "RC=%ERRORLEVEL%" +goto :finish +:elevate +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 set "RC=%ERRORLEVEL%" +:finish +echo [%date% %time%] PC_Fixer.ps1 exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause +) exit /b %RC% diff --git a/pc-corruption-fixer/PC_Fixer.ps1 b/pc-corruption-fixer/PC_Fixer.ps1 index 2545d69..da5c34e 100644 --- a/pc-corruption-fixer/PC_Fixer.ps1 +++ b/pc-corruption-fixer/PC_Fixer.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - PC Corruption Fixer v7.2 - Advanced system repair and diagnostics toolkit. + PC Corruption Fixer v7.2.1 - Advanced system repair and diagnostics toolkit. .DESCRIPTION Repairs corrupted system files, fixes Windows Update safely, clears caches, resets networking, deep-cleans components, checks disk health, manages @@ -8,7 +8,7 @@ exports a full HTML health report. All operations are logged. .NOTES Requires Administrator. Run via Fix_Corruption.bat or right-click -> Run with PowerShell. - Log file saved to: $env:USERPROFILE\Desktop\PC_Fixer_Log_*.txt + Logs: $env:LOCALAPPDATA\WindowsPCToolkit\Logs. Use -DebugLog for transcripts. v7.1 CHANGES: - REBUILT: AI Feature Privacy [23] is now policy-only and fully reversible. @@ -81,6 +81,11 @@ #> #Requires -RunAsAdministrator +param([switch]$DebugLog,[string]$ExpectedUserSid) +if ($ExpectedUserSid -and $ExpectedUserSid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value) { + Write-Error 'Elevation switched Windows accounts. Launch from an administrator account belonging to the intended user.' + exit 1 +} $ProgressPreference = 'SilentlyContinue' @@ -88,10 +93,12 @@ $ProgressPreference = 'SilentlyContinue' # CONFIGURATION # ============================================================================ -$Script:Version = '7.2' -$Script:LogDir = "$env:USERPROFILE\Desktop" +$Script:Version = '7.2.1' +$Script:DebugLog = $DebugLog -or $env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1' +$Script:LogDir = Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\PCFixer' $Script:LogName = "PC_Fixer_Log_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" $Script:LogPath = Join-Path $Script:LogDir $Script:LogName +if (-not $Script:DebugLog) { $Script:LogPath=Join-Path $Script:LogDir 'PCFixer.log' } $Script:LogKeep = 15 # number of past log files to retain $Script:StartTime = Get-Date $Script:Results = [ordered]@{} @@ -99,6 +106,19 @@ $Script:HasFailure = $false $Script:StepNum = 0 $Script:TotalSteps = 0 $Script:StayAwakeArmed = $false +$Script:RestartRequired = $false +[void][IO.Directory]::CreateDirectory($Script:LogDir) + +function Write-FixerDiagnostic { + param([string]$Kind,[string]$Message) + if (-not $Script:DebugLog -and $Kind -notin @('Fail','Warn')) { return } + try { + $path=Join-Path $Script:LogDir 'PCFixer.log' + if ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).Length -gt 2MB) { Move-Item -LiteralPath $path -Destination ($path+'.1') -Force } + [pscustomobject]@{Time=(Get-Date).ToString('o');Version=$Script:Version;Kind=$Kind;Message=$Message} | ConvertTo-Json -Compress | Add-Content -LiteralPath $path -Encoding UTF8 + } catch { Write-Warning ('Could not write fixer diagnostic log: '+$_.Exception.Message) } +} +Write-FixerDiagnostic Info ("Start; LogDir=$Script:LogDir; Debug=$Script:DebugLog; Windows=$env:SystemRoot") # Absolute path to the trusted System32 directory. Every native Windows tool # and every DLL this script touches is resolved under here so that a binary @@ -164,7 +184,7 @@ function Get-UsageBar { # ============================================================================ try { - Start-Transcript -Path $Script:LogPath -Append -ErrorAction SilentlyContinue | Out-Null + if ($Script:DebugLog) { Start-Transcript -Path $Script:LogPath -Append -ErrorAction Stop | Out-Null } } catch { } # Prune old logs so the Desktop doesn't fill up (keeps the newest $Script:LogKeep). @@ -194,11 +214,11 @@ public static class FixerStayAwake { public const uint ES_CONTINUOUS = 0x80000000; public const uint ES_SYSTEM_REQUIRED = 0x00000001; public const uint ES_AWAYMODE_REQUIRED = 0x00000040; - public static void PreventSleep() { - SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED); + public static bool PreventSleep() { + return SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) != 0; } - public static void AllowSleep() { - SetThreadExecutionState(ES_CONTINUOUS); + public static bool AllowSleep() { + return SetThreadExecutionState(ES_CONTINUOUS) != 0; } } '@ @@ -210,19 +230,19 @@ public static class FixerStayAwake { function Enable-StayAwake { try { if ('FixerStayAwake' -as [type]) { - [FixerStayAwake]::PreventSleep() + if (-not [FixerStayAwake]::PreventSleep()) { throw 'Windows rejected the temporary stay-awake request.' } $Script:StayAwakeArmed = $true } - } catch { } + } catch { Write-Status Warn $_.Exception.Message } } function Disable-StayAwake { try { if (('FixerStayAwake' -as [type]) -and $Script:StayAwakeArmed) { - [FixerStayAwake]::AllowSleep() + if (-not [FixerStayAwake]::AllowSleep()) { throw 'Windows rejected stay-awake release.' } $Script:StayAwakeArmed = $false } - } catch { } + } catch { Write-Status Warn $_.Exception.Message } } function Resolve-SystemTool { @@ -236,8 +256,8 @@ function Resolve-SystemTool { [string]$BaseDir ) $leaf = Split-Path -Leaf $Name - $root = if ([string]::IsNullOrWhiteSpace($BaseDir)) { $Script:Sys32 } else { $BaseDir } - $winRoot = $env:SystemRoot.TrimEnd('\') + '\' + $root = [IO.Path]::GetFullPath($(if ([string]::IsNullOrWhiteSpace($BaseDir)) { $Script:Sys32 } else { $BaseDir })) + $winRoot = [IO.Path]::GetFullPath($env:SystemRoot).TrimEnd('\') + '\' if (-not ($root.TrimEnd('\') + '\').StartsWith($winRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return $null } @@ -325,7 +345,9 @@ function Invoke-NativeProcessSafe { $exe = Resolve-SystemTool $FilePath -BaseDir $BaseDir if (-not $exe) { return [PSCustomObject]@{ ExitCode = 9009; Output = "Tool not found: $FilePath" } } $quoted = foreach ($arg in $ArgumentList) { - if ($arg -match '[\s"]') { '"' + ($arg -replace '"','\"') + '"' } else { $arg } + if ($arg.Length -eq 0 -or $arg -match '[\s"]') { + '"'+[regex]::Replace($arg,'(\\*)("|$)',{param($m) $slashes=$m.Groups[1].Value; if($m.Groups[2].Value -eq '"'){$slashes+$slashes+'\"'}else{$slashes+$slashes}})+'"' + } else { $arg } } $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $exe @@ -424,6 +446,8 @@ function Write-Status { $m = $map[$Type] Write-Host "$($m.Tag)" -ForegroundColor $m.Color -NoNewline Write-Host $Text -ForegroundColor $(if ($Type -eq 'Info') { 'Gray' } else { 'White' }) + Write-FixerDiagnostic $Type $Text + if ($Type -eq 'Fail') { $Script:HasFailure=$true } } function Write-StepHeader { @@ -511,16 +535,16 @@ function Show-Dashboard { # Disk try { - $disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -EA Stop + $disk = Get-CimInstance Win32_LogicalDisk -Filter ("DeviceID='{0}'" -f $env:SystemDrive) -EA Stop $freeGB = [math]::Round($disk.FreeSpace / 1GB, 1) $totalGB = [math]::Round($disk.Size / 1GB, 1) $pctFree = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1) $diskColor = if ($pctFree -lt 10) { 'Red' } elseif ($pctFree -lt 20) { 'Yellow' } else { 'White' } - Write-Host ' C: Drive : ' -ForegroundColor Gray -NoNewline + Write-Host (' {0} Drive : ' -f $env:SystemDrive) -ForegroundColor Gray -NoNewline Write-Host "$freeGB GB free / $totalGB GB " -ForegroundColor $diskColor -NoNewline Write-Host (Get-UsageBar (100 - $pctFree) 14) -ForegroundColor DarkCyan -NoNewline Write-Host " $pctFree% free" -ForegroundColor $diskColor - } catch { Write-Host ' C: Drive : (unavailable)' -ForegroundColor DarkGray } + } catch { Write-Host (' {0} Drive : (unavailable)' -f $env:SystemDrive) -ForegroundColor DarkGray } # RAM try { @@ -619,7 +643,7 @@ function Show-Menu { Write-Host " $($B.H * 62)" -ForegroundColor DarkGray Write-Host '' Write-Host ' [1] * FULL REPAIR (Recommended)' -ForegroundColor Green - Write-Host ' SFC + conditional DISM + safe caches + network health + disk scan' + Write-Host ' DISM then SFC + safe caches + network health + disk scan' Write-Host '' Write-Host ' SYSTEM REPAIR' -ForegroundColor DarkCyan Write-Host " $($B.MH * 40)" -ForegroundColor DarkGray @@ -694,6 +718,12 @@ function Invoke-SfcScan { if ($Total -gt 0) { Write-StepHeader 'SFC - System File Check' $Step $Total } else { Write-StepHeader 'SFC - System File Check' } + $pending=Test-PendingReboot + if ($Script:RestartRequired -or ($pending.Pending -and @($pending.Reasons | Where-Object { $_ -in @('Windows Update','Component Based Servicing') }).Count)) { + Write-Status Warn 'Finish the pending Windows restart before SFC repair.' + $Script:Results['SFC']='SKIP - Update restart pending' + return + } Write-Status Info 'Scanning for corrupted or missing system files...' Write-Status Info 'This may take 5-15 minutes. Sleep is blocked for this run.' @@ -741,12 +771,12 @@ function Invoke-DismRepair { Enable-StayAwake $sw=[Diagnostics.Stopwatch]::StartNew() try { - $result=Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online','/Cleanup-Image','/RestoreHealth' + $result=Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online','/Cleanup-Image','/RestoreHealth','/NoRestart' Write-Host $result.Output if ($result.ExitCode -in @(0,3010)) { Write-Status OK 'DISM command completed. Review its output for the repair verdict.' $Script:Results['DISM']='COMPLETED - Review DISM output' - if ($result.ExitCode -eq 3010) { Write-Status Warn 'Windows requests a restart. No automatic restart will occur.' } + if ($result.ExitCode -eq 3010) { $Script:RestartRequired=$true; Write-Status Warn 'Windows requests a restart. No automatic restart will occur.' } } else { Write-Status Fail "DISM exited with code $($result.ExitCode). Review its output before continuing." $Script:Results['DISM']="FAIL - Exit code $($result.ExitCode)" @@ -772,25 +802,28 @@ function Invoke-CacheCleanup { $sw = [System.Diagnostics.Stopwatch]::StartNew() # DNS Flush + $cacheAttention=$false try { - $null = Invoke-Native-Quiet -FilePath 'ipconfig.exe' -ArgumentList '/flushdns' + $flush = Invoke-Native-Quiet -FilePath 'ipconfig.exe' -ArgumentList '/flushdns' + if ($flush.ExitCode -ne 0) { throw "ipconfig /flushdns failed (exit $($flush.ExitCode)): $($flush.Output)" } Write-Status OK 'DNS resolver cache flushed.' - } catch { Write-Status Warn 'DNS flush skipped.' } + } catch { $cacheAttention=$true; Write-Status Warn ('DNS flush skipped: '+$_.Exception.Message) } # Reuse the suite's age, protected-data and link guards in every repair path. try { Import-Module (Join-Path $PSScriptRoot '..\Modules\Toolkit.Core.psm1') -Force -DisableNameChecking Write-Status OK (Invoke-SuiteTempCleanup) - } catch { Write-Status Warn ('Temp cleanup skipped: '+$_.Exception.Message) } + } catch { $cacheAttention=$true; Write-Status Warn ('Temp cleanup skipped: '+$_.Exception.Message) } # Thumbnail cache try { $thumbPath = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer" if (Test-Path $thumbPath) { - $thumbs = Get-ChildItem "$thumbPath\thumbcache_*.db" -EA SilentlyContinue + $thumbs = @(Get-ChildItem -LiteralPath $thumbPath -Filter 'thumbcache_*.db' -File -EA Stop) if ($thumbs) { - $thumbs | Remove-Item -Force -EA SilentlyContinue - Write-Status OK "Thumbnail cache cleared ($($thumbs.Count) files)." + $removed=0 + foreach ($thumb in $thumbs) { try { Remove-Item -LiteralPath $thumb.FullName -Force -EA Stop; $removed++ } catch { $cacheAttention=$true } } + Write-Status Info "Thumbnail cache: $removed of $($thumbs.Count) files removed; locked files retained." } else { Write-Status OK 'Thumbnail cache already clean.' } @@ -826,8 +859,8 @@ function Invoke-CacheCleanup { # Deleting SoftwareDistribution\Download breaks in-progress updates # and can corrupt the WU database. Use [7] Repair Windows Update instead. - if ($totalTempFiles -gt 0 -and $totalTempRemoved -eq 0) { - $Script:Results['Caches'] = 'WARN - Temp files were in use; nothing could be removed' + if ($cacheAttention) { + $Script:Results['Caches'] = 'WARN - Cache cleanup completed with skipped/failed requests; review output' } else { $Script:Results['Caches'] = 'PASS - Safe caches cleaned (Update data and Recycle Bin preserved unless requested)' } @@ -863,7 +896,7 @@ function Backup-NetworkDnsState { } if (@($adapters).Count -eq 0) { throw 'No adapters could be backed up.' } $doh = @() - if (Get-Command Get-DnsClientDohServerAddress -EA Stop) { + if (Get-Command Get-DnsClientDohServerAddress -EA SilentlyContinue) { $doh = @(Get-DnsClientDohServerAddress -EA Stop | ForEach-Object { [PSCustomObject]@{ ServerAddress = $_.ServerAddress @@ -921,8 +954,9 @@ function Invoke-NetworkHealthRefresh { if ($Total -gt 0) { Write-StepHeader 'Network Health + DNS Refresh' $Step $Total } else { Write-StepHeader 'Network Health + DNS Refresh' } $sw = [System.Diagnostics.Stopwatch]::StartNew() - $null = Invoke-Native-Quiet -FilePath 'ipconfig.exe' -ArgumentList '/flushdns' - Write-Status OK 'DNS resolver cache flushed. Adapter DNS and DoH settings preserved.' + $flush=Invoke-Native-Quiet -FilePath 'ipconfig.exe' -ArgumentList '/flushdns' + if ($flush.ExitCode -eq 0) { Write-Status OK 'DNS resolver cache flushed. Adapter DNS and DoH settings preserved.' } + else { Write-Status Warn "DNS cache flush failed (exit $($flush.ExitCode)): $($flush.Output)" } $dnsOK = $false; $httpsOK = $false try { Resolve-DnsName -Name 'www.microsoft.com' -DnsOnly -EA Stop | Out-Null; $dnsOK = $true; Write-Status OK 'DNS resolution test passed.' } catch { Write-Status Warn "DNS resolution test failed: $_" } @@ -976,11 +1010,12 @@ function Invoke-NetworkReset { Write-Host ' VLAN, VPN, and adapter settings. This tool backs up DNS/DoH only.' -ForegroundColor Yellow $tcpChoice = Read-Host ' Also reset the TCP/IP stack? (y/N)' $tcpReset = $false + $resetErrors=0 if ($tcpChoice -eq 'y' -or $tcpChoice -eq 'Y') { Write-Status Info 'Resetting TCP/IP stack...' $tcp = Invoke-Native-Quiet -FilePath 'netsh.exe' -ArgumentList 'int', 'ip', 'reset' if ($tcp.ExitCode -eq 0) { Write-Status OK 'TCP/IP reset.'; $tcpReset = $true } - else { Write-Status Warn "TCP/IP reset exited with code $($tcp.ExitCode)." } + else { $resetErrors++; Write-Status Warn "TCP/IP reset exited with code $($tcp.ExitCode): $($tcp.Output)" } } else { Write-Status Skip 'TCP/IP reset skipped; custom adapter settings preserved.' } @@ -999,7 +1034,7 @@ function Invoke-NetworkReset { if ($fwChoice -eq 'y' -or $fwChoice -eq 'Y') { $fw = Invoke-Native-Quiet -FilePath 'netsh.exe' -ArgumentList 'advfirewall', 'reset' if ($fw.ExitCode -eq 0) { Write-Status OK 'Firewall reset to defaults.'; $firewallReset = $true } - else { Write-Status Warn "Firewall reset exited with code $($fw.ExitCode)." } + else { $resetErrors++; Write-Status Warn "Firewall reset exited with code $($fw.ExitCode): $($fw.Output)" } } else { Write-Status Skip 'Firewall reset skipped (custom rules preserved).' } @@ -1012,7 +1047,7 @@ function Invoke-NetworkReset { $parts = @('Winsock') if ($tcpReset) { $parts += 'TCP/IP' } if ($firewallReset) { $parts += 'firewall' } - $Script:Results['Network'] = "PASS - $($parts -join ' + ') reset; DNS state restored" + $Script:Results['Network'] = if($resetErrors){"WARN - $($parts -join ' + ') reset; $resetErrors requested reset(s) failed; DNS state restored"}else{"PASS - $($parts -join ' + ') reset; DNS state restored"} } Write-Status Info 'A reboot is required to complete Winsock or TCP/IP reset changes.' $sw.Stop() @@ -1028,21 +1063,34 @@ function Invoke-DeepCleanup { if ($Total -gt 0) { Write-StepHeader 'DISM - Deep Component Cleanup' $Step $Total } else { Write-StepHeader 'DISM - Deep Component Cleanup' } + $pending=Test-PendingReboot + if ($Script:RestartRequired -or ($pending.Pending -and @($pending.Reasons | Where-Object { $_ -in @('Windows Update','Component Based Servicing') }).Count)) { + Write-Status Warn 'Finish the pending Windows restart before component cleanup.' + $Script:Results['DeepClean']='SKIP - Update restart pending' + return + } $sw = [System.Diagnostics.Stopwatch]::StartNew() - + $clean2=$null + Enable-StayAwake + try { Write-Status Info 'Analyzing component store...' Write-Host '' $null = Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/AnalyzeComponentStore' Write-Host '' Write-Status Info 'Starting component cleanup...' - $clean1 = Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/StartComponentCleanup' - if ($clean1.ExitCode -eq 0) { + $clean1 = Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/StartComponentCleanup', '/NoRestart' + if ($clean1.ExitCode -in @(0,3010)) { Write-Status OK 'Component cleanup complete.' } else { Write-Status Warn "Component cleanup exited with code $($clean1.ExitCode)." } + if ($clean1.ExitCode -eq 3010) { $Script:RestartRequired=$true } + if ($clean1.ExitCode -ne 0) { + $Script:Results['DeepClean']='WARN - Cleanup needs attention or a restart; ResetBase not attempted' + return + } # ResetBase WARNING - this prevents uninstalling recent updates Write-Host '' @@ -1053,9 +1101,10 @@ function Invoke-DeepCleanup { $rbChoice = Read-Host ' Run ResetBase? (y/N)' if ($rbChoice -eq 'y' -or $rbChoice -eq 'Y') { Write-Status Info 'Running ResetBase...' - $clean2 = Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/StartComponentCleanup', '/ResetBase' - if ($clean2.ExitCode -eq 0) { + $clean2 = Invoke-Native -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/StartComponentCleanup', '/ResetBase', '/NoRestart' + if ($clean2.ExitCode -in @(0,3010)) { Write-Status OK 'ResetBase complete - superseded components removed.' + if ($clean2.ExitCode -eq 3010) { $Script:RestartRequired=$true } } else { Write-Status Fail "ResetBase exited with code $($clean2.ExitCode)." $Script:HasFailure = $true @@ -1064,14 +1113,14 @@ function Invoke-DeepCleanup { Write-Status Skip 'ResetBase skipped by user.' } - $sw.Stop() - Write-Status Info "Elapsed: $(Get-Elapsed $sw)" - - if ($clean1.ExitCode -eq 0) { + if ($clean2 -and $clean2.ExitCode -notin @(0,3010)) { + $Script:Results['DeepClean']='FAIL - ResetBase incomplete; review native output' + } elseif ($clean1.ExitCode -eq 0) { $Script:Results['DeepClean'] = 'PASS - Component store cleaned' } else { $Script:Results['DeepClean'] = 'WARN - Component cleanup incomplete' } + } finally { $sw.Stop(); Disable-StayAwake; Write-Status Info "Elapsed: $(Get-Elapsed $sw)" } } # ============================================================================ @@ -1280,11 +1329,12 @@ function Invoke-ServiceHealthCheck { $running = 0 $stopped = 0 $issues = @() + $unavailable=0;$disabled=0 foreach ($svc in $criticalServices) { try { $service = Get-Service -Name $svc.Name -EA SilentlyContinue - if ($null -eq $service) { continue } + if ($null -eq $service) { $unavailable++; Write-Status Warn ("{0,-38} Unavailable" -f $svc.Display); continue } if ($service.Status -eq 'Running') { Write-Status OK ("{0,-38} Running" -f $svc.Display) @@ -1293,7 +1343,8 @@ function Invoke-ServiceHealthCheck { # Deliberately disabled (by the user or a tweak tool) - report # it, but don't fight the user's configuration. Write-Status Warn ("{0,-38} Disabled (not auto-started)" -f $svc.Display) - } elseif ($service.StartType -eq 'Manual') { + $disabled++ + } elseif ($service.Status -eq 'Stopped' -and $service.StartType -eq 'Manual') { # Demand-start services (BITS, etc.) sit stopped by design and # Windows launches them when needed - not a problem. Write-Status Info ("{0,-38} Stopped (starts on demand)" -f $svc.Display) @@ -1309,6 +1360,7 @@ function Invoke-ServiceHealthCheck { } } } catch { + $unavailable++ Write-Status Warn ("{0,-38} Could not check" -f $svc.Display) } } @@ -1330,9 +1382,12 @@ function Invoke-ServiceHealthCheck { } } $Script:Results['Services'] = "WARN - $stopped stopped (attempted restart)" + } elseif ($unavailable -or $disabled) { + Write-Status Warn "$disabled disabled and $unavailable unavailable service(s); existing configuration preserved." + $Script:Results['Services']="WARN - $disabled disabled, $unavailable unavailable services" } else { - Write-Status OK 'All critical services are healthy.' - $Script:Results['Services'] = 'PASS - All critical services running' + Write-Status OK 'Critical services are running or ready on demand.' + $Script:Results['Services'] = 'PASS - Services running or ready on demand' } $sw.Stop() @@ -1367,11 +1422,12 @@ function Invoke-StoreReset { $count = 0 $errors = 0 try { - $appxPackages = Get-AppxPackage -AllUsers -EA SilentlyContinue + $appxPackages = @(Get-AppxPackage -EA Stop) foreach ($pkg in $appxPackages) { try { + if ([string]::IsNullOrWhiteSpace($pkg.InstallLocation)) { continue } $manifestPath = Join-Path $pkg.InstallLocation 'AppxManifest.xml' - if (Test-Path $manifestPath) { + if (Test-Path -LiteralPath $manifestPath) { Add-AppxPackage -DisableDevelopmentMode -Register $manifestPath -EA Stop | Out-Null $count++ } @@ -1395,6 +1451,14 @@ function Invoke-StoreReset { # FEATURE: EVENT LOG ERROR SCAN # ============================================================================ +function Get-FixerEvents { + param([string]$LogName,[datetime]$Since,[int]$MaxEvents=20) + $readErrors=@() + $events=@(Get-WinEvent -FilterHashtable @{LogName=$LogName;Level=1,2;StartTime=$Since} -MaxEvents $MaxEvents -EA SilentlyContinue -ErrorVariable readErrors) + if (@($readErrors | Where-Object { $_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*' }).Count) { throw "$LogName event log read failed: $($readErrors[0].Exception.Message)" } + return $events +} + function Invoke-EventLogScan { Write-StepHeader 'Event Log Error Scan' @@ -1406,15 +1470,12 @@ function Invoke-EventLogScan { $since = (Get-Date).AddHours(-72) $totalErrors = 0 $totalCritical = 0 + $logUnavailable=$false # System log Write-Status Step 'System Event Log:' try { - $sysEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'System' - Level = 1,2 # Critical and Error - StartTime = $since - } -MaxEvents 20 -EA SilentlyContinue + $sysEvents = @(Get-FixerEvents -LogName 'System' -Since $since) if ($sysEvents -and $sysEvents.Count -gt 0) { $crit = ($sysEvents | Where-Object { $_.Level -eq 1 }).Count @@ -1435,6 +1496,7 @@ function Invoke-EventLogScan { Write-Status OK 'No critical/error events in last 72 hours.' } } catch { + $logUnavailable=$true Write-Status Warn 'Could not read System event log.' } @@ -1443,11 +1505,7 @@ function Invoke-EventLogScan { # Application log Write-Status Step 'Application Event Log:' try { - $appEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Application' - Level = 1,2 - StartTime = $since - } -MaxEvents 20 -EA SilentlyContinue + $appEvents = @(Get-FixerEvents -LogName 'Application' -Since $since) if ($appEvents -and $appEvents.Count -gt 0) { $crit = ($appEvents | Where-Object { $_.Level -eq 1 }).Count @@ -1468,6 +1526,7 @@ function Invoke-EventLogScan { Write-Status OK 'No critical/error events in last 72 hours.' } } catch { + $logUnavailable=$true Write-Status Warn 'Could not read Application event log.' } @@ -1477,7 +1536,8 @@ function Invoke-EventLogScan { $sw.Stop() Write-Status Info "Elapsed: $(Get-Elapsed $sw)" - if ($totalCritical -gt 0) { + if ($logUnavailable) { $Script:Results['EventLog']='WARN - One or more event logs could not be read' } + elseif ($totalCritical -gt 0) { $Script:Results['EventLog'] = "WARN - $totalCritical critical, $totalErrors errors" } elseif ($totalErrors -gt 0) { $Script:Results['EventLog'] = "INFO - $totalErrors errors (no critical)" @@ -1501,30 +1561,17 @@ function Invoke-QuickHealthScan { # 1. SFC verify-only Write-Status Step 'Checking system file integrity (verify only)...' $sfcResult = Invoke-Native-Quiet -FilePath 'sfc.exe' -ArgumentList '/verifyonly' - if ($sfcResult.ExitCode -eq 0) { - Write-Status OK 'System file integrity OK.' - } elseif ($sfcResult.ExitCode -eq 3) { - Write-Status Warn 'Integrity violations detected. Run SFC repair [2] to fix.' - $issues++ - } elseif ($sfcResult.ExitCode -eq 2) { - Write-Status Warn 'SFC could not complete the integrity check.' - $issues++ - } else { - Write-Status Info "SFC verify returned code $($sfcResult.ExitCode)." - } + Write-Host $sfcResult.Output + if ($sfcResult.ExitCode -eq 0) { Write-Status Info 'SFC verification command completed; review the localized integrity verdict above.' } + else { Write-Status Warn "SFC verification returned code $($sfcResult.ExitCode); review its output and CBS.log."; $issues++ } Write-Host '' # 2. DISM check health Write-Status Step 'Checking component store health...' $dismResult = Invoke-Native-Quiet -FilePath 'DISM.exe' -ArgumentList '/Online', '/Cleanup-Image', '/CheckHealth' - if ($dismResult.Output -match 'No component store corruption detected') { - Write-Status OK 'No component store corruption on record (CheckHealth reflects prior scans).' - } elseif ($dismResult.Output -match 'repairable|corruption detected') { - Write-Status Warn 'Component store has repairable corruption. Run DISM repair [2].' - $issues++ - } else { - Write-Status Info 'Component store check completed.' - } + Write-Host $dismResult.Output + if ($dismResult.ExitCode -eq 0) { Write-Status Info 'DISM CheckHealth completed; review its recorded-corruption verdict above. This is not a fresh full scan.' } + else { Write-Status Warn "DISM CheckHealth returned code $($dismResult.ExitCode)."; $issues++ } Write-Host '' # 3. Disk space @@ -1565,14 +1612,16 @@ function Invoke-QuickHealthScan { $svcIssues = 0 foreach ($svcName in $criticalSvc) { $svc = Get-Service -Name $svcName -EA SilentlyContinue - if ($svc -and $svc.Status -ne 'Running') { + if (-not $svc) { Write-Status Warn "$svcName is unavailable."; $svcIssues++; $issues++ } + elseif ($svc.Status -eq 'Stopped' -and $svc.StartType -eq 'Manual') { Write-Status Info "$svcName is idle (starts on demand)." } + elseif ($svc.Status -ne 'Running') { Write-Status Fail "$svcName ($($svc.DisplayName)) is NOT running." $svcIssues++ $issues++ } } if ($svcIssues -eq 0) { - Write-Status OK 'All critical services running.' + Write-Status OK 'Critical services are running or ready on demand.' } Write-Host '' @@ -1617,14 +1666,14 @@ function Invoke-QuickHealthScan { Write-Host '' Write-Host " $($B.H * 62)" -ForegroundColor DarkGray if ($issues -eq 0) { - Write-Status OK 'HEALTH SCAN COMPLETE - No issues found.' + Write-Status Info 'HEALTH SCAN COMPLETE - Review SFC/DISM output; no additional issues were detected by these checks.' } else { Write-Status Warn "HEALTH SCAN COMPLETE - $issues issue(s) found." Write-Status Info 'Consider running the appropriate repair options.' } Write-Status Info "Elapsed: $(Get-Elapsed $sw)" - $Script:Results['QuickScan'] = if ($issues -eq 0) { 'PASS - Healthy' } else { "WARN - $issues issue(s) found" } + $Script:Results['QuickScan'] = if ($issues -eq 0) { 'COMPLETED - Review native integrity verdicts' } else { "WARN - $issues issue(s) found; review native integrity verdicts" } } # ============================================================================ @@ -1672,8 +1721,10 @@ function Invoke-WUHealthCheck { $issues++ } elseif ($svc.Status -eq 'Running') { Write-Status OK ("{0,-34} Running" -f $s.Display) - } else { + } elseif ($svc.Status -eq 'Stopped' -and $svc.StartType -eq 'Manual') { Write-Status Info ("{0,-34} Stopped (starts on demand)" -f $s.Display) + } else { + Write-Status Warn ("{0,-34} {1} / {2}" -f $s.Display,$svc.Status,$svc.StartType); $issues++ } } Write-Host '' @@ -1698,7 +1749,7 @@ function Invoke-WUHealthCheck { Write-Status Step 'Disk space for updates:' try { - $sys = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -EA Stop + $sys = Get-CimInstance Win32_LogicalDisk -Filter ("DeviceID='{0}'" -f $env:SystemDrive) -EA Stop $freeGB = [math]::Round($sys.FreeSpace / 1GB, 1) if ($freeGB -lt 20) { Write-Status Warn "$freeGB GB free - big feature updates want ~20 GB."; $issues++ } else { Write-Status OK "$freeGB GB free." } @@ -1810,7 +1861,7 @@ function Invoke-NetworkDiagnostics { Write-Status OK "$($d.ServerAddress) -> $($d.DohTemplate) [$fb]" } } else { - Write-Status Info 'No custom DoH templates registered. Run DNS_Set_Quad9_Mullvad.bat for encrypted DNS.' + Write-Status Info 'No DoH templates registered. Use Encrypted DNS in the dashboard for supported providers.' } } else { Write-Status Info 'DoH cmdlets not available on this Windows build (need Win11 / Server 2022+).' @@ -1858,7 +1909,7 @@ function Invoke-IconCacheRebuild { $removed = 0 try { Write-Status Info 'Stopping Explorer...' - Get-Process -Name explorer -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue + Get-Process -Name explorer -EA SilentlyContinue | Where-Object { $_.SessionId -eq [Diagnostics.Process]::GetCurrentProcess().SessionId } | Stop-Process -Force -EA Stop Start-Sleep -Seconds 2 $cacheDir = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer" @@ -1878,7 +1929,7 @@ function Invoke-IconCacheRebuild { # Windows 11 sometimes auto-restarts Explorer; only launch it if needed # (starting it while the shell is running just opens a folder window). Start-Sleep -Seconds 1 - if (-not (Get-Process -Name explorer -EA SilentlyContinue)) { + if (-not (Get-Process -Name explorer -EA SilentlyContinue | Where-Object { $_.SessionId -eq [Diagnostics.Process]::GetCurrentProcess().SessionId })) { Write-Status Info 'Restarting Explorer...' Start-Process -FilePath (Join-Path $env:SystemRoot 'explorer.exe') } @@ -1922,26 +1973,9 @@ function Invoke-TimeSyncRepair { Write-Status OK 'Time resynchronized successfully.' $Script:Results['TimeSync'] = 'PASS - Resynced' } else { - Write-Status Warn 'Resync failed. Re-registering the time service...' - $null = Invoke-Native-Quiet -FilePath 'w32tm.exe' -ArgumentList '/unregister' - $reg = Invoke-Native-Quiet -FilePath 'w32tm.exe' -ArgumentList '/register' - if ($reg.ExitCode -ne 0) { - Write-Status Fail "Time service re-registration failed (exit code $($reg.ExitCode)). W32Time may be missing - run 'w32tm /register' manually as Administrator." - $Script:Results['TimeSync'] = 'FAIL - Re-register failed' - $Script:HasFailure = $true - } else { - [void](Start-ServiceSafely 'W32Time') - Start-Sleep -Seconds 2 - $r2 = Invoke-Native-Quiet -FilePath 'w32tm.exe' -ArgumentList '/resync', '/force' - if ($r2.ExitCode -eq 0) { - Write-Status OK 'Time service re-registered and resynced.' - $Script:Results['TimeSync'] = 'PASS - Re-registered + resynced' - } else { - Write-Status Fail 'Time sync still failing. Check internet and the BIOS clock/battery.' - $Script:Results['TimeSync'] = 'FAIL - Resync failed' - $Script:HasFailure = $true - } - } + Write-Status Fail ("Time resync failed (exit {0}): {1}" -f $r.ExitCode,$r.Output) + Write-Status Info 'Existing NTP/domain time configuration was preserved. Check connectivity, time source and BIOS clock.' + $Script:Results['TimeSync']='FAIL - Resync failed; existing time configuration retained' } Write-Host '' @@ -2224,26 +2258,26 @@ function Export-HealthReport { @{ Name = 'CryptSvc'; Display = 'Cryptographic Services' })) { $svc = Get-Service -Name $svcDef.Name -EA SilentlyContinue if ($svc) { - $cls = if ($svc.Status -eq 'Running') { 'good' } else { 'bad' } - $svcRows += "$(ConvertTo-HtmlSafe $svcDef.Display)$($svc.Status)" + $cls = if ($svc.Status -eq 'Running' -or ($svc.Status -eq 'Stopped' -and $svc.StartType -eq 'Manual')) { 'good' } else { 'warn' } + $svcRows += "$(ConvertTo-HtmlSafe $svcDef.Display)$($svc.Status) / $($svc.StartType)" } } $evtSummary = '' - try { - $since = $now.AddHours(-72) - foreach ($logName in 'System', 'Application') { - $evts = @(Get-WinEvent -FilterHashtable @{ LogName = $logName; Level = 1, 2; StartTime = $since } -MaxEvents 50 -EA SilentlyContinue) + $since = $now.AddHours(-72) + foreach ($logName in 'System', 'Application') { + try { + $evts = @(Get-FixerEvents -LogName $logName -Since $since -MaxEvents 50) $cls = if ($evts.Count -gt 10) { 'warn' } else { 'good' } $evtSummary += "$logName log$($evts.Count) critical/error events (72h)" - } - } catch { } + } catch { $evtSummary += "$logName logUnavailable; event log could not be read"; Write-Status Warn $_.Exception.Message } + } $resultRows = '' if ($Script:Results.Count -gt 0) { foreach ($key in $Script:Results.Keys) { $val = $Script:Results[$key] - $cls = if ($val -match 'FAIL') { 'bad' } elseif ($val -match 'WARN|SKIP') { 'warn' } else { 'good' } + $cls = if ($val -match 'FAIL') { 'bad' } elseif ($val -match 'WARN|SKIP|ATTENTION|Partial') { 'warn' } else { 'good' } $resultRows += "$(ConvertTo-HtmlSafe $key)$(ConvertTo-HtmlSafe $val)" } } else { @@ -2373,7 +2407,7 @@ function Invoke-PerfCounterRepair { Write-Status Step 'Resyncing WMI performance data...' $rs = Invoke-Native-Quiet -FilePath 'winmgmt.exe' -ArgumentList '/resyncperf' if ($rs.ExitCode -eq 0) { Write-Status OK 'WMI counters resynced.' } - else { Write-Status Warn "WMI resync returned exit $($rs.ExitCode)." } + else { Write-Status Warn "WMI resync returned exit $($rs.ExitCode)."; $okAll=$false } $sw.Stop() Write-Host '' @@ -2425,13 +2459,14 @@ function Invoke-OrphanServiceScan { } $orphans = @() + Import-Module (Join-Path $PSScriptRoot '..\Modules\Toolkit.Core.psm1') -DisableNameChecking -ErrorAction Stop foreach ($s in $services) { $exe = Get-ServiceExePath $s.PathName if (-not $exe) { continue } $exe = [System.Environment]::ExpandEnvironmentVariables($exe) # Only judge absolute paths; anything else can't be checked reliably. - if (-not [System.IO.Path]::IsPathRooted($exe)) { continue } - if (-not (Test-Path -LiteralPath $exe)) { + if ($s.State -ne 'Stopped') { continue } + if (Test-SuiteMissingExecutable $exe) { $orphans += [PSCustomObject]@{ Name = $s.Name; Display = $s.DisplayName Exe = $exe; StartMode = $s.StartMode @@ -2443,7 +2478,7 @@ function Invoke-OrphanServiceScan { Write-Host '' if ($orphans.Count -eq 0) { - Write-Status OK 'No orphaned services found - every service binary exists.' + Write-Status OK 'No stopped services with a confirmed missing local executable were found.' $Script:Results['OrphanSvc'] = 'PASS - No orphans' } else { foreach ($o in $orphans) { @@ -2462,10 +2497,15 @@ function Invoke-OrphanServiceScan { foreach ($o in $orphans) { $ans = Read-Host " Delete service '$($o.Name)' ($($o.Display))? (y/N)" if ($ans -eq 'y' -or $ans -eq 'Y') { + $current=Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $o.Name.Replace("'","''")) -EA Stop + if (-not $current -or $current.State -ne 'Stopped' -or -not (Test-SuiteMissingExecutable ([Environment]::ExpandEnvironmentVariables((Get-ServiceExePath $current.PathName))))) { + Write-Status Warn "Service state/path changed or is no longer confirmed missing: $($o.Name). Not deleting." + continue + } # Back up the service registration before deleting - sc delete has no undo. $backupFile = Join-Path $backupDir ("$($o.Name)_$(Get-Date -Format 'yyyyMMdd_HHmmss').reg") $exp = Invoke-Native-Quiet -FilePath 'reg.exe' -ArgumentList 'export', "HKLM\SYSTEM\CurrentControlSet\Services\$($o.Name)", $backupFile, '/y' - if ($exp.ExitCode -ne 0) { + if ($exp.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $backupFile -PathType Leaf) -or (Get-Item -LiteralPath $backupFile).Length -eq 0) { Write-Status Fail "Registry backup failed for $($o.Name) - not deleting." continue } @@ -2533,7 +2573,7 @@ function Invoke-FullRepair { Enable-StayAwake try { switch ($step.Func) { - 'sfc' { Invoke-SfcScan -Step $stepNum -Total $steps.Count } + 'sfc' { if ($Script:Results['DISM'] -like 'COMPLETED*' -and -not $Script:RestartRequired) { Invoke-SfcScan -Step $stepNum -Total $steps.Count } else { $Script:Results['SFC']='SKIP - DISM needs attention or restart'; Write-Status Warn 'SFC deferred until DISM completes without a pending restart.' } } 'dism' { Invoke-DismRepair -Step $stepNum -Total $steps.Count } 'caches' { Invoke-CacheCleanup -Step $stepNum -Total $steps.Count } 'networkhealth' { Invoke-NetworkHealthRefresh -Step $stepNum -Total $steps.Count } @@ -2627,6 +2667,11 @@ function Restore-AiFeatureSnapshot { $snapshot = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json if($snapshot.Schema -ne 1 -or $snapshot.Computer -ne $env:COMPUTERNAME -or $snapshot.User -ne [Security.Principal.WindowsIdentity]::GetCurrent().Name){throw 'This AI policy snapshot belongs to another computer/user or has an unsupported schema.'} $restoreErrors = 0 + $allowed=@(Get-AiFeaturePolicyDefinitions) + foreach ($entry in @($snapshot.Entries)) { + if (-not @($allowed | Where-Object { $_.Path -eq $entry.Path -and $_.Name -eq $entry.Name }).Count) { throw 'AI policy snapshot contains an unsupported registry target.' } + if ($entry.ValueExisted -isnot [bool] -or $entry.PathExisted -isnot [bool] -or ($entry.ValueExisted -and $entry.Kind -notin @('String','ExpandString','MultiString','Binary','QWord','DWord'))) { throw 'AI policy snapshot contains an invalid registry state.' } + } foreach ($entry in @($snapshot.Entries)) { if ([bool]$entry.ValueExisted) { if (-not (Test-Path -LiteralPath $entry.Path)) { New-Item -Path $entry.Path -Force | Out-Null } @@ -2639,10 +2684,10 @@ function Restore-AiFeatureSnapshot { default { 'DWord' } } $restoreValue = switch ($propertyType) { - 'Binary' { [byte[]]@($entry.Value) } - 'MultiString' { [string[]]@($entry.Value) } - 'QWord' { [uint64]$entry.Value } - 'DWord' { [uint32]$entry.Value } + 'Binary' { ,([byte[]]@($entry.Value)) } + 'MultiString' { ,([string[]]@($entry.Value)) } + 'QWord' { [long]$entry.Value } + 'DWord' { [int]$entry.Value } default { [string]$entry.Value } } try { New-ItemProperty -Path $entry.Path -Name $entry.Name -PropertyType $propertyType -Value $restoreValue -Force -ErrorAction Stop | Out-Null } @@ -2653,7 +2698,7 @@ function Restore-AiFeatureSnapshot { } try { $actual=Get-AiRegistryEntryState $entry.Path $entry.Name - if($actual.ValueExisted -ne $entry.ValueExisted -or ($actual.ValueExisted -and ($actual.Kind -ne $entry.Kind -or (ConvertTo-Json -InputObject $actual.Value -Compress) -ne (ConvertTo-Json -InputObject $entry.Value -Compress)))){throw 'AI policy restore read-back failed.'} + if($actual.ValueExisted -ne $entry.ValueExisted -or ($actual.ValueExisted -and ($actual.Kind -ne $entry.Kind -or (ConvertTo-Json -InputObject $actual.Value -Compress) -cne (ConvertTo-Json -InputObject $entry.Value -Compress)))){throw 'AI policy restore read-back failed.'} } catch { $restoreErrors++ } } foreach ($pathState in @($snapshot.Entries | Group-Object Path)) { @@ -2752,7 +2797,7 @@ function Show-Summary { } else { foreach ($key in $Script:Results.Keys) { $val = $Script:Results[$key] - $color = if ($val -match 'FAIL') { 'Red' } elseif ($val -match 'WARN|SKIP') { 'Yellow' } else { 'Green' } + $color = if ($val -match 'FAIL') { 'Red' } elseif ($val -match 'WARN|SKIP|ATTENTION|Partial') { 'Yellow' } else { 'Green' } $line = " [$key] $val" if ($line.Length -gt $inner) { $line = $line.Substring(0, $inner - 3) + '...' } Write-Host " $($B.V)" -ForegroundColor Cyan -NoNewline @@ -2780,15 +2825,15 @@ function Show-Summary { Write-Host " $($B.BL)$($B.H * $inner)$($B.BR)" -ForegroundColor Cyan Write-Host '' - if (-not $Script:HasFailure -and $Script:Results.Count -gt 0) { - Write-Host ' [OK] ALL OPERATIONS COMPLETED SUCCESSFULLY' -ForegroundColor Green + if (-not $Script:HasFailure -and $Script:Results.Count -gt 0 -and -not @($Script:Results.Values | Where-Object { $_ -match 'WARN|FAIL|SKIP|ATTENTION|Partial' }).Count) { + Write-Host ' [OK] Commands completed. Review individual results and native integrity verdicts.' -ForegroundColor Green } elseif ($Script:Results.Count -gt 0) { Write-Host ' [!!] Some operations had warnings or failures - review the log.' -ForegroundColor Yellow } # Check if restart is needed $pbr = Test-PendingReboot - if ($pbr.Pending -or $Script:Results.ContainsKey('SFC') -or $Script:Results.ContainsKey('DeepClean')) { + if ($pbr.Pending -or $Script:RestartRequired) { Write-Host '' Write-Host ' ==> RESTART YOUR PC NOW for all changes to take effect.' -ForegroundColor Yellow } @@ -2811,7 +2856,7 @@ function Main { switch ($sel) { '1' { Invoke-FullRepair } - '2' { Invoke-DismRepair; Invoke-SfcScan } + '2' { Invoke-DismRepair; if ($Script:Results['DISM'] -like 'COMPLETED*' -and -not $Script:RestartRequired) { Invoke-SfcScan } else { Write-Status Warn 'SFC repair deferred until DISM completes without a pending restart.' } } '3' { $rb = Read-Host ' Also empty the Recycle Bin? (y/N)'; Invoke-CacheCleanup -IncludeRecycleBin:($rb -match '^[Yy]$') } '4' { Invoke-NetworkReset } '5' { Invoke-DeepCleanup } @@ -2858,6 +2903,7 @@ try { Write-Host '' Write-Host ' [!!] FATAL ERROR:' -ForegroundColor Red Write-Host " $_" -ForegroundColor Red + Write-FixerDiagnostic Fail ($_ | Out-String) Write-Host '' Write-Host " Partial log saved to: $Script:LogPath" -ForegroundColor DarkGray Write-Host '' diff --git a/pc-corruption-fixer/README.md b/pc-corruption-fixer/README.md index 587ebe1..fbc670c 100644 --- a/pc-corruption-fixer/README.md +++ b/pc-corruption-fixer/README.md @@ -1,4 +1,4 @@ -# PC Corruption Fixer 7.2 +# PC Corruption Fixer 7.2.1 Official Windows repair and diagnostics with logs, explicit advanced choices and retained backups. Use [Repair & tools in the dashboard](../README.md) or `Fix_Corruption.bat`. Keep the complete toolkit extracted together. @@ -6,7 +6,7 @@ Official Windows repair and diagnostics with logs, explicit advanced choices and The dashboard runs **DISM /RestoreHealth → SFC /scannow**, saves native output and exit codes, blocks pending update restarts and never forces reboot. This follows [Microsoft's repair sequence](https://support.microsoft.com/en-us/topic/use-the-system-file-checker-tool-to-repair-missing-or-corrupted-system-files-79aa86cb-ca52-166a-92a3-966e85d4094e). -The console's Full Repair adds guarded aged-temp/cache cleanup, DNS cache/connectivity refresh and online disk checks. It does not reset Winsock/TCP/IP/firewall or alter adapter DNS. SFC's actual output supplies the corruption verdict; no guessed numeric exit mapping claims files were repaired. +The console's Full Repair adds guarded aged-temp/cache cleanup, DNS cache/connectivity refresh and online disk checks. It does not reset Winsock/TCP/IP/firewall or alter adapter DNS. SFC's actual output supplies the corruption verdict; no guessed numeric exit mapping claims files were repaired. SFC is deferred if DISM fails or needs a restart. Component cleanup also waits for pending update/servicing restarts, and DISM runs with `/NoRestart`. ## Windows Update repair @@ -18,6 +18,8 @@ Explicit Winsock reset is separate from Full Repair. Deeper TCP/IP reset needs i Other console tools: disk scan, Store/AppX repair, event logs, service/driver/startup diagnostics, time sync, disk-space analysis, performance counters, HTML report and reversible AI/browser privacy policies. AI policy support depends on Windows edition/build and browser version. Policies can delete Recall snapshots and browser built-in models; undo restores policy values, not deleted data. Use the dashboard for modern Copilot app removal and checked Recall feature disabling. ResetBase, service removal, strict policies and other advanced operations are separate choices. -Console logs are written to the Desktop. Network/AI/service backups are under `%ProgramData%\WindowsPCToolkit\PCFixer`. Dashboard repair reports are in `WindowsPCToolkit\Suite\Runs`. +Store registration repair uses packages already installed for the current account. It does not register another user's apps into your account. A failed time resync preserves your existing NTP/domain configuration. Orphan-service deletion requires a stopped service and a confirmed missing `.exe` on a ready local fixed drive; disconnected/network paths and access failures are not treated as missing programs. + +Warnings/errors are logged to `%LOCALAPPDATA%\WindowsPCToolkit\Logs\PCFixer\PCFixer.log`, with version and cause. For detailed transcripts and configuration/state diagnostics, launch `PC_Fixer.ps1 -DebugLog` or set `WINDOWS_PC_TOOLKIT_DEBUG=1` for the launching process. Debug logging is off by default. JSONL diagnostics rotate at 2 MB; at most 15 debug transcripts are retained. HTML reports use the same log folder. Network/AI/service backups are under `%ProgramData%\WindowsPCToolkit\PCFixer`. Dashboard repair reports are in `WindowsPCToolkit\Suite\Runs`. Windows 10/11, administrator rights and built-in Windows PowerShell 5.1. General repair is logged maintenance rather than a reversible registry optimization. diff --git a/pc-gaming-optimizer/Fix_VR_PCVR.ps1 b/pc-gaming-optimizer/Fix_VR_PCVR.ps1 index c62c395..e92bed9 100644 --- a/pc-gaming-optimizer/Fix_VR_PCVR.ps1 +++ b/pc-gaming-optimizer/Fix_VR_PCVR.ps1 @@ -1,4 +1,9 @@ #requires -version 5.1 +param([string]$ExpectedUserSid) +if ($ExpectedUserSid -and $ExpectedUserSid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value) { + Write-Error 'Elevation switched Windows accounts. Launch from an administrator account belonging to the intended user.' + exit 1 +} $ErrorActionPreference='Stop' Import-Module (Join-Path $PSScriptRoot 'Modules\Optimizer.Core.psm1') -Force Initialize-GamingToolkit diff --git a/pc-gaming-optimizer/Modules/Optimizer.Core.psm1 b/pc-gaming-optimizer/Modules/Optimizer.Core.psm1 index 8b6bff3..df7e945 100644 --- a/pc-gaming-optimizer/Modules/Optimizer.Core.psm1 +++ b/pc-gaming-optimizer/Modules/Optimizer.Core.psm1 @@ -2,15 +2,28 @@ Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' -$script:ToolkitVersion = '5.0' +$script:ToolkitVersion = '5.0.1' $script:StateRoot = Join-Path $env:ProgramData 'WindowsPCToolkit\GamingOptimizer' $script:SnapshotRoot = Join-Path $script:StateRoot 'Snapshots' $script:LogRoot = Join-Path $script:StateRoot 'Logs' +$script:DiagnosticPath = Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\GamingOptimizer.log' +$script:DebugLog = $env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1' + +function Write-GamingDiagnostic { + param([string]$Kind,[string]$Message) + if (-not $script:DebugLog -and $Kind -notin @('FAIL','WARN')) { return } + try { + [void][IO.Directory]::CreateDirectory((Split-Path $script:DiagnosticPath -Parent)) + if ((Test-Path -LiteralPath $script:DiagnosticPath) -and (Get-Item -LiteralPath $script:DiagnosticPath).Length -gt 2MB) { Move-Item -LiteralPath $script:DiagnosticPath -Destination ($script:DiagnosticPath+'.1') -Force } + [pscustomobject]@{Time=(Get-Date).ToString('o');Version=$script:ToolkitVersion;Kind=$Kind;Message=$Message} | ConvertTo-Json -Compress | Add-Content -LiteralPath $script:DiagnosticPath -Encoding UTF8 + } catch { Write-Warning ('Could not write gaming diagnostic log: '+$_.Exception.Message) } +} function Initialize-GamingToolkit { foreach ($path in @($script:StateRoot, $script:SnapshotRoot, $script:LogRoot)) { if (-not (Test-Path -LiteralPath $path)) { New-Item -ItemType Directory -Path $path -Force | Out-Null } } + Write-GamingDiagnostic INFO ("Start; StateRoot=$script:StateRoot; SnapshotRoot=$script:SnapshotRoot; Debug=$script:DebugLog") } function Test-IsAdministrator { @@ -28,6 +41,7 @@ function Write-ToolStatus { $color = switch ($Kind) { 'OK' {'Green'} 'INFO' {'Cyan'} 'WARN' {'Yellow'} 'FAIL' {'Red'} default {'White'} } $tag = switch ($Kind) { 'OK' {'[OK]'} 'INFO' {'[i]'} 'WARN' {'[!!]'} 'FAIL' {'[X]'} default {'[>>]'} } Write-Host (' {0} {1}' -f $tag, $Message) -ForegroundColor $color + Write-GamingDiagnostic $Kind $Message } function Write-GamingHeader { @@ -61,13 +75,15 @@ function Set-RegistryValueExact { 'DWord' {'DWord'} 'MultiString' {'MultiString'} 'QWord' {'QWord'} default {'String'} } $typedValue = switch ($propertyType) { - 'Binary' { [byte[]]@($Value) } + 'Binary' { ,([byte[]]@($Value)) } 'DWord' { [int]$Value } 'QWord' { [long]$Value } - 'MultiString' { [string[]]@($Value) } + 'MultiString' { ,([string[]]@($Value)) } default { $Value } } New-ItemProperty -LiteralPath $Path -Name $Name -Value $typedValue -PropertyType $propertyType -Force | Out-Null + $actual=Get-RegistryValueState $Path $Name + if (-not $actual.Exists -or $actual.Kind -ne $propertyType -or (ConvertTo-Json -InputObject $actual.Value -Compress) -cne (ConvertTo-Json -InputObject $typedValue -Compress)) { throw "Registry write verification failed: $Path\$Name" } } function Restore-RegistryValueState { @@ -91,6 +107,7 @@ function Get-ActivePowerSchemeGuid { function Get-BcdOverrideStates { $names = @('useplatformclock','useplatformtick','disabledynamictick','tscsyncpolicy') $output = @(& "$env:SystemRoot\System32\bcdedit.exe" /enum '{current}' 2>$null) + if ($LASTEXITCODE -ne 0) { throw "Boot configuration could not be read (exit $LASTEXITCODE); no changes are safe." } foreach ($name in $names) { $line = $output | Where-Object { $_ -match ("^\s*{0}\s+(.+)$" -f [regex]::Escape($name)) } | Select-Object -First 1 if ($line -and $line -match ("^\s*{0}\s+(.+)$" -f [regex]::Escape($name))) { @@ -105,6 +122,7 @@ function Get-NetworkAdapterStates { $rsc = Get-NetAdapterRsc -Name $adapter.Name -ErrorAction SilentlyContinue [pscustomobject]@{ Name=$adapter.Name + InterfaceGuid=$adapter.InterfaceGuid.ToString() InterfaceDescription=$adapter.InterfaceDescription RssAvailable=[bool]($null -ne $rss) RssEnabled=if($rss){[bool]$rss.Enabled}else{$false} @@ -155,13 +173,13 @@ function Get-GamingRegistryTargets { ) $ifRoot='HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' if(Test-Path -LiteralPath $ifRoot){ - foreach($key in Get-ChildItem -LiteralPath $ifRoot -ErrorAction SilentlyContinue){ + foreach($key in Get-ChildItem -LiteralPath $ifRoot -ErrorAction Stop){ foreach($name in @('TcpAckFrequency','TCPNoDelay','TcpDelAckTicks')){$targets += @{Path=$key.PSPath;Name=$name}} } } $displayRoot='HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}' if(Test-Path -LiteralPath $displayRoot){ - foreach($key in Get-ChildItem -LiteralPath $displayRoot -ErrorAction SilentlyContinue){ + foreach($key in Get-ChildItem -LiteralPath $displayRoot -ErrorAction Stop){ foreach($name in @('PowerMizerEnable','PowerMizerLevel','PowerMizerLevelAC','PerLevelGPUCacheSize','ShaderCacheSize','MSISupported','MessageNumberLimit')){$targets += @{Path=$key.PSPath;Name=$name}} } } @@ -169,22 +187,23 @@ function Get-GamingRegistryTargets { } function New-GamingSnapshot { - param([string]$Reason='Manual change') + param([string]$Reason='Manual change',[object[]]$RegistryTargets=(Get-GamingRegistryTargets),[switch]$IncludeServices,[switch]$IncludeBcd,[switch]$IncludeNetwork) Initialize-GamingToolkit $stamp = Get-Date -Format 'yyyyMMdd_HHmmss_fff' $path = Join-Path $script:SnapshotRoot ("gaming_{0}.json" -f $stamp) - $registry = foreach ($t in Get-GamingRegistryTargets) { Get-RegistryValueState -Path $t.Path -Name $t.Name } - $services = foreach ($name in @('SysMain')) { - $svc = Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $name) -ErrorAction SilentlyContinue + $registry = foreach ($t in $RegistryTargets) { Get-RegistryValueState -Path $t.Path -Name $t.Name } + $services = if ($IncludeServices) { foreach ($name in @('SysMain')) { + $svc = Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $name) -ErrorAction Stop if ($svc) { [pscustomobject]@{Name=$name; StartMode=$svc.StartMode; State=$svc.State} } - } + } } $snapshot = [pscustomobject]@{ - Schema=2; ToolkitVersion=$script:ToolkitVersion; Created=(Get-Date).ToString('o'); Reason=$Reason - PowerScheme=(Get-ActivePowerSchemeGuid); Registry=@($registry); Services=@($services) - BcdOverrides=@(Get-BcdOverrideStates); NetworkAdapters=@(Get-NetworkAdapterStates) + Schema=3; ToolkitVersion=$script:ToolkitVersion; Created=(Get-Date).ToString('o'); Reason=$Reason + Computer=$env:COMPUTERNAME; UserSid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value + PowerScheme=$null; Registry=@($registry | Where-Object { $null -ne $_ }); Services=@($services | Where-Object { $null -ne $_ }) + BcdOverrides=@(if($IncludeBcd){Get-BcdOverrideStates}); NetworkAdapters=@(if($IncludeNetwork){Get-NetworkAdapterStates}) } $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 - Set-Content -LiteralPath (Join-Path $script:StateRoot 'latest_snapshot.txt') -Value $path -Encoding ASCII + Set-Content -LiteralPath (Join-Path $script:StateRoot 'latest_snapshot.txt') -Value $path -Encoding UTF8 Write-ToolStatus OK ("Safety snapshot saved: {0}" -f $path) return $path } @@ -193,7 +212,8 @@ function Get-LatestGamingSnapshotPath { $pointer = Join-Path $script:StateRoot 'latest_snapshot.txt' if (Test-Path -LiteralPath $pointer) { $path = (Get-Content -LiteralPath $pointer -Raw).Trim() - if (Test-Path -LiteralPath $path) { return $path } + if ([IO.Path]::GetFullPath($path).StartsWith([IO.Path]::GetFullPath($script:SnapshotRoot).TrimEnd('\')+'\',[StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $path -PathType Leaf)) { return $path } + Write-ToolStatus WARN 'Snapshot pointer is outside the local snapshot folder or missing; using the newest local snapshot instead.' } $latest = Get-ChildItem -LiteralPath $script:SnapshotRoot -Filter 'gaming_*.json' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($latest) { return $latest.FullName } @@ -206,33 +226,68 @@ function Restore-LatestGamingSnapshot { $path = Get-LatestGamingSnapshotPath if (-not $path) { Write-ToolStatus WARN 'No optimizer snapshot exists yet.'; return } $snap = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json - foreach ($state in @($snap.Registry)) { Restore-RegistryValueState -State $state } + if ($snap.Schema -notin @(1,2,3)) { throw 'Unsupported gaming snapshot schema.' } + if ($snap.Schema -eq 3 -and ($snap.Computer -ne $env:COMPUTERNAME -or $snap.UserSid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) { throw 'This gaming snapshot belongs to another computer or user.' } + if ($snap.Schema -lt 3) { Write-ToolStatus WARN 'Legacy snapshot has no recorded computer/user identity and may include unrelated settings. Restore only your own local backup.' } + $allowed=@(Get-GamingRegistryTargets) + foreach ($state in @($snap.Registry)) { + if (-not @($allowed | Where-Object { $_.Path -eq $state.Path -and $_.Name -eq $state.Name }).Count) { throw 'Gaming snapshot contains an unsupported registry target.' } + if ($state.Exists -isnot [bool] -or ($state.Exists -and $state.Kind -notin @('String','ExpandString','Binary','DWord','MultiString','QWord'))) { throw 'Gaming snapshot contains an invalid registry state.' } + } + foreach ($svcState in @($snap.Services)) { + if ($svcState.Name -ne 'SysMain' -or $svcState.StartMode -notin @('Auto','Manual','Disabled') -or $svcState.State -notin @('Running','Stopped')) { throw 'Gaming snapshot contains an unsupported service state.' } + } + if ($snap.PowerScheme -and $snap.PowerScheme -notmatch '^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$') { throw 'Gaming snapshot contains an invalid power scheme.' } + if ($snap.PSObject.Properties.Name -contains 'BcdOverrides') { + foreach ($state in @($snap.BcdOverrides)) { if ($state.Name -notin @('useplatformclock','useplatformtick','disabledynamictick','tscsyncpolicy') -or $state.Exists -isnot [bool]) { throw 'Gaming snapshot contains an unsupported boot state.' } } + } + Write-ToolStatus WARN 'Restoring a console snapshot can replace later manual changes. Review its path and scope before continuing.' + if ((Read-Host (" Restore snapshot '$path'? (y/N)")) -notmatch '^[Yy]$') { return } + foreach ($state in @($snap.Registry)) { + Restore-RegistryValueState -State $state + $actual=Get-RegistryValueState $state.Path $state.Name + if ($actual.Exists -ne $state.Exists -or ($actual.Exists -and ($actual.Kind -ne $state.Kind -or (ConvertTo-Json -InputObject $actual.Value -Compress) -cne (ConvertTo-Json -InputObject $state.Value -Compress)))) { throw "Registry restore verification failed: $($state.Path)\$($state.Name)" } + } foreach ($svcState in @($snap.Services)) { $startType = switch ($svcState.StartMode) { 'Auto' {'Automatic'} 'Manual' {'Manual'} 'Disabled' {'Disabled'} default {$null} } - if ($startType) { Set-Service -Name $svcState.Name -StartupType $startType -ErrorAction SilentlyContinue } - if ($svcState.State -eq 'Running') { Start-Service -Name $svcState.Name -ErrorAction SilentlyContinue } - elseif ($svcState.State -eq 'Stopped') { Stop-Service -Name $svcState.Name -Force -ErrorAction SilentlyContinue } + if ($startType) { Set-Service -Name $svcState.Name -StartupType $startType -ErrorAction Stop } + if ($svcState.State -eq 'Running') { Start-Service -Name $svcState.Name -ErrorAction Stop } + elseif ($svcState.State -eq 'Stopped') { Stop-Service -Name $svcState.Name -Force -ErrorAction Stop } + $actualService=Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $svcState.Name) -ErrorAction Stop + if ($actualService.State -ne $svcState.State -or $actualService.StartMode -ne $svcState.StartMode) { throw "Service restore verification failed: $($svcState.Name)" } } if ($snap.PSObject.Properties.Name -contains 'NetworkAdapters') { foreach ($adapterState in @($snap.NetworkAdapters)) { - $adapter = Get-NetAdapter -Name $adapterState.Name -ErrorAction SilentlyContinue - if (-not $adapter) { continue } + $adapter = if ($adapterState.PSObject.Properties.Name -contains 'InterfaceGuid') { Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceGuid.ToString() -eq $adapterState.InterfaceGuid } | Select-Object -First 1 } else { Get-NetAdapter -Name $adapterState.Name -ErrorAction Stop } + if (-not $adapter) { throw "Original adapter not found: $($adapterState.Name)" } if ($adapterState.RssAvailable) { - if ($adapterState.RssEnabled) { Enable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue } - else { Disable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue } + if ($adapterState.RssEnabled) { Enable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction Stop } + else { Disable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction Stop } + if ([bool](Get-NetAdapterRss -Name $adapter.Name -ErrorAction Stop).Enabled -ne [bool]$adapterState.RssEnabled) { throw "RSS restore verification failed: $($adapter.Name)" } } if ($adapterState.RscAvailable) { - Set-NetAdapterRsc -Name $adapter.Name -IPv4Enabled ([bool]$adapterState.RscIPv4Enabled) -IPv6Enabled ([bool]$adapterState.RscIPv6Enabled) -NoRestart -ErrorAction SilentlyContinue + Set-NetAdapterRsc -Name $adapter.Name -IPv4Enabled ([bool]$adapterState.RscIPv4Enabled) -IPv6Enabled ([bool]$adapterState.RscIPv6Enabled) -NoRestart -ErrorAction Stop + $rsc=Get-NetAdapterRsc -Name $adapter.Name -ErrorAction Stop + if ([bool]$rsc.IPv4Enabled -ne [bool]$adapterState.RscIPv4Enabled -or [bool]$rsc.IPv6Enabled -ne [bool]$adapterState.RscIPv6Enabled) { throw "RSC restore verification failed: $($adapter.Name)" } } } } if ($snap.PSObject.Properties.Name -contains 'BcdOverrides') { foreach ($bcdState in @($snap.BcdOverrides)) { + $current=Get-BcdOverrideStates | Where-Object Name -eq $bcdState.Name + if (-not $current) { throw "Unknown BCD setting: $($bcdState.Name)" } if ($bcdState.Exists) { & "$env:SystemRoot\System32\bcdedit.exe" /set '{current}' $bcdState.Name $bcdState.Value 2>$null | Out-Null } - else { & "$env:SystemRoot\System32\bcdedit.exe" /deletevalue '{current}' $bcdState.Name 2>$null | Out-Null } + elseif ($current.Exists) { & "$env:SystemRoot\System32\bcdedit.exe" /deletevalue '{current}' $bcdState.Name 2>$null | Out-Null } + else { continue } + if ($LASTEXITCODE -ne 0) { throw "BCD restore failed for $($bcdState.Name) (exit $LASTEXITCODE)." } + $verified=Get-BcdOverrideStates | Where-Object Name -eq $bcdState.Name + if ($verified.Exists -ne $bcdState.Exists -or ($verified.Exists -and $verified.Value -cne $bcdState.Value)) { throw "BCD restore verification failed: $($bcdState.Name)" } } } - if ($snap.PowerScheme) { & "$env:SystemRoot\System32\powercfg.exe" /setactive $snap.PowerScheme | Out-Null } + if ($snap.PowerScheme) { + & "$env:SystemRoot\System32\powercfg.exe" /setactive $snap.PowerScheme | Out-Null + if ($LASTEXITCODE -ne 0 -or (Get-ActivePowerSchemeGuid) -ne $snap.PowerScheme) { throw 'Power scheme restore verification failed.' } + } Write-ToolStatus OK ("Restored exact state from {0}" -f $path) Write-ToolStatus INFO 'A sign-out or reboot may be needed for every setting to refresh.' } @@ -253,7 +308,7 @@ function Invoke-HagsManager { Write-Host ' [1] Enable HAGS [2] Disable HAGS [0] Leave unchanged' $choice = Read-Host ' Selection' if ($choice -notin @('1','2')) { Write-ToolStatus INFO 'No change made.'; return } - New-GamingSnapshot -Reason 'HAGS change' | Out-Null + New-GamingSnapshot -Reason 'HAGS change' -RegistryTargets @(@{Path='HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers';Name='HwSchMode'}) | Out-Null $value = if ($choice -eq '1') { 2 } else { 1 } Set-RegistryValueExact -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' -Name 'HwSchMode' -Value $value -Kind DWord Write-ToolStatus OK 'HAGS setting changed. Reboot required.' @@ -263,7 +318,8 @@ function Invoke-HagsManager { function Invoke-SafeGamingProfile { param([switch]$DisableCaptures) Assert-Administrator - New-GamingSnapshot -Reason 'Safe gaming profile' | Out-Null + $names=@('AllowAutoGameMode','AutoGameModeEnabled'); if($DisableCaptures){$names+=@('GameDVR_Enabled','AppCaptureEnabled')} + New-GamingSnapshot -Reason 'Safe gaming profile' -RegistryTargets @(Get-GamingRegistryTargets | Where-Object { $_.Name -in $names }) | Out-Null Set-RegistryValueExact -Path 'HKCU:\Software\Microsoft\GameBar' -Name 'AllowAutoGameMode' -Value 1 -Kind DWord Set-RegistryValueExact -Path 'HKCU:\Software\Microsoft\GameBar' -Name 'AutoGameModeEnabled' -Value 1 -Kind DWord if ($DisableCaptures) { @@ -285,15 +341,27 @@ function Invoke-ShaderCacheCleanup { (Join-Path $env:LOCALAPPDATA 'AMD\GLCache'), (Join-Path $env:LOCALAPPDATA 'Intel\ShaderCache') ) - $removed = 0; $bytes = [int64]0 + $removed = 0; $skipped=0; $bytes = [int64]0 + $localRoot=[IO.Path]::GetFullPath($env:LOCALAPPDATA).TrimEnd('\') foreach ($path in $paths) { if (-not (Test-Path -LiteralPath $path)) { continue } - foreach ($item in @(Get-ChildItem -LiteralPath $path -Force -Recurse -File -ErrorAction SilentlyContinue)) { - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } - try { $length=$item.Length; Remove-Item -LiteralPath $item.FullName -Force -ErrorAction Stop; $bytes+=$length; $removed++ } catch { } + $ancestor=$path; $linked=$false + while ($ancestor -and $ancestor.Length -ge $localRoot.Length) { + if ((Get-Item -LiteralPath $ancestor -Force -ErrorAction Stop).Attributes -band [IO.FileAttributes]::ReparsePoint) { $linked=$true; break } + $ancestor=Split-Path $ancestor -Parent + } + if ($linked) { $skipped++; continue } + $pending=New-Object 'Collections.Generic.Stack[string]'; $pending.Push($path) + while ($pending.Count) { + foreach ($item in @(Get-ChildItem -LiteralPath $pending.Pop() -Force -ErrorAction Stop)) { + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { $skipped++; continue } + if ($item.PSIsContainer) { $pending.Push($item.FullName); continue } + try { $length=$item.Length; Remove-Item -LiteralPath $item.FullName -Force -ErrorAction Stop; $bytes+=$length; $removed++ } catch { $skipped++ } + } } } Write-ToolStatus OK ("Removed {0} shader-cache files ({1:N1} MB)." -f $removed, ($bytes / 1MB)) + if ($skipped) { Write-ToolStatus WARN "$skipped linked or locked cache items skipped." } Write-ToolStatus INFO 'The first launch of each game may stutter while shaders rebuild. Use this for troubleshooting, not routine maintenance.' } @@ -315,7 +383,7 @@ function Show-GpuAudit { } function Get-PhysicalNetAdapters { - @(Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -ne 'Disabled' }) + @(Get-NetAdapter -Physical -ErrorAction Stop | Where-Object { $_.Status -ne 'Disabled' }) } function Show-NetworkAudit { @@ -338,9 +406,10 @@ function Show-NetworkAudit { function Remove-RegistryValueIfPresent { param([string]$Path,[string]$Name) if (Test-Path -LiteralPath $Path) { - $key = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue + $key = Get-Item -LiteralPath $Path -ErrorAction Stop if ($key -and ($key.GetValueNames() -contains $Name)) { - Remove-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction SilentlyContinue + Remove-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction Stop + if ((Get-RegistryValueState $Path $Name).Exists) { throw "Registry removal verification failed: $Path\$Name" } Write-ToolStatus OK ("Removed legacy override: {0}\{1}" -f $Path,$Name) } } @@ -351,20 +420,17 @@ function Invoke-LegacyNetworkCleanup { Write-ToolStatus WARN 'This removes old suite latency tweaks, then returns core TCP settings to supported defaults. DNS is preserved.' $confirm = Read-Host ' Continue? (y/N)' if ($confirm -notmatch '^[Yy]$') { Write-ToolStatus INFO 'Cancelled.'; return } - New-GamingSnapshot -Reason 'Legacy network tweak cleanup' | Out-Null + New-GamingSnapshot -Reason 'Legacy network tweak cleanup' -RegistryTargets @(Get-GamingRegistryTargets | Where-Object { $_.Name -in @('NetworkThrottlingIndex','SystemResponsiveness','TcpAckFrequency','TCPNoDelay','TcpDelAckTicks') }) -IncludeNetwork | Out-Null $sysProfile='HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile' Remove-RegistryValueIfPresent $sysProfile 'NetworkThrottlingIndex' Remove-RegistryValueIfPresent $sysProfile 'SystemResponsiveness' $ifRoot='HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' if (Test-Path -LiteralPath $ifRoot) { - foreach ($key in Get-ChildItem -LiteralPath $ifRoot -ErrorAction SilentlyContinue) { + foreach ($key in Get-ChildItem -LiteralPath $ifRoot -ErrorAction Stop) { foreach ($name in @('TcpAckFrequency','TCPNoDelay','TcpDelAckTicks')) { Remove-RegistryValueIfPresent $key.PSPath $name } } } - foreach($adapter in Get-PhysicalNetAdapters){ - Enable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue - Enable-NetAdapterRsc -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue - } + Enable-GamingSupportedOffloads Write-ToolStatus OK 'Legacy Nagle/QoS/MMCSS overrides removed; supported RSS/RSC offloads enabled where available.' Write-ToolStatus INFO 'TCP autotuning was preserved rather than guessed.' Write-ToolStatus INFO 'Adapter DNS addresses and DoH entries were not touched.' @@ -381,11 +447,18 @@ function Show-StorageAudit { function Invoke-SafeRetrim { Assert-Administrator - $volumes = @(Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.FileSystem -eq 'NTFS' }) - foreach ($v in $volumes) { - try { Optimize-Volume -DriveLetter $v.DriveLetter -ReTrim -Verbose -ErrorAction Stop; Write-ToolStatus OK ("ReTrim completed on {0}:" -f $v.DriveLetter) } - catch { Write-ToolStatus WARN ("ReTrim skipped on {0}: {1}" -f $v.DriveLetter,$_.Exception.Message) } + $physical=@(Get-PhysicalDisk -ErrorAction Stop | Where-Object { $_.MediaType -eq 'SSD' -and -not [string]::IsNullOrWhiteSpace($_.SerialNumber) }) + $count=0 + foreach ($disk in Get-Disk -ErrorAction Stop) { + if ([string]::IsNullOrWhiteSpace($disk.SerialNumber)) { continue } + $matches=@($physical | Where-Object { $_.SerialNumber.Trim() -eq $disk.SerialNumber.Trim() }) + if ($matches.Count -ne 1) { continue } + foreach ($v in @(Get-Partition -DiskNumber $disk.Number -ErrorAction Stop | Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.FileSystem -in @('NTFS','ReFS') })) { + Optimize-Volume -DriveLetter $v.DriveLetter -ReTrim -Verbose -ErrorAction Stop + $count++; Write-ToolStatus OK ("ReTrim completed on {0}:" -f $v.DriveLetter) + } } + Write-ToolStatus INFO "$count confirmed SSD volumes processed; HDD and unknown media preserved." } function Show-MemoryAudit { @@ -400,9 +473,8 @@ function Show-MemoryAudit { function Show-TimerAudit { Write-Host '' Write-Host ' TIMER / BOOT CONFIG AUDIT' -ForegroundColor Cyan - $bcd = & "$env:SystemRoot\System32\bcdedit.exe" /enum '{current}' 2>$null - $interesting = $bcd | Where-Object { $_ -match 'useplatformclock|useplatformtick|disabledynamictick|tscsyncpolicy' } - if ($interesting) { $interesting | ForEach-Object { Write-Host (' ' + $_) -ForegroundColor Yellow } } + $interesting = @(Get-BcdOverrideStates | Where-Object Exists) + if ($interesting) { $interesting | ForEach-Object { Write-Host (' {0}: {1}' -f $_.Name,$_.Value) -ForegroundColor Yellow } } else { Write-ToolStatus OK 'No legacy timer overrides were found in the current boot entry.' } try { $timer = Get-CimInstance Win32_PerfFormattedData_PerfOS_System -ErrorAction Stop @@ -416,10 +488,8 @@ function Invoke-LegacyTimerCleanup { Write-ToolStatus WARN 'This deletes old BCD timer overrides and restores Windows automatic timer selection.' $confirm=Read-Host ' Continue? (y/N)' if ($confirm -notmatch '^[Yy]$') { return } - New-GamingSnapshot -Reason 'Legacy timer override cleanup' | Out-Null - foreach ($name in @('useplatformclock','useplatformtick','disabledynamictick','tscsyncpolicy')) { - & "$env:SystemRoot\System32\bcdedit.exe" /deletevalue '{current}' $name 2>$null | Out-Null - } + New-GamingSnapshot -Reason 'Legacy timer override cleanup' -RegistryTargets @() -IncludeBcd | Out-Null + Remove-GamingBcdOverrides Write-ToolStatus OK 'Legacy BCD timer overrides removed. Reboot required.' } @@ -442,7 +512,7 @@ function Invoke-InputProfile { Assert-Administrator $choice=Read-Host ' Disable Windows mouse acceleration for 1:1 pointer movement? (y/N)' if ($choice -notmatch '^[Yy]$') { Write-ToolStatus INFO 'No change made.'; return } - New-GamingSnapshot -Reason 'Mouse acceleration change' | Out-Null + New-GamingSnapshot -Reason 'Mouse acceleration change' -RegistryTargets @(Get-GamingRegistryTargets | Where-Object { $_.Name -in @('MouseSpeed','MouseThreshold1','MouseThreshold2') }) | Out-Null Set-RegistryValueExact -Path 'HKCU:\Control Panel\Mouse' -Name 'MouseSpeed' -Value '0' -Kind String Set-RegistryValueExact -Path 'HKCU:\Control Panel\Mouse' -Name 'MouseThreshold1' -Value '0' -Kind String Set-RegistryValueExact -Path 'HKCU:\Control Panel\Mouse' -Name 'MouseThreshold2' -Value '0' -Kind String @@ -515,7 +585,7 @@ function Invoke-GameBooster { } catch { Write-ToolStatus FAIL $_.Exception.Message } finally { if ($p -and $null -ne $old) { try { if (-not $p.HasExited) { $p.PriorityClass=$old } } catch { Write-ToolStatus WARN 'The game priority could not be restored.' } } - Disable-StayAwake + try { Disable-StayAwake } catch { Write-ToolStatus WARN ('Stay-awake release failed: '+$_.Exception.Message) } } } @@ -536,9 +606,9 @@ function Invoke-LegacyOptimizerRepair { Write-ToolStatus WARN 'This removes unsupported tweaks left by v3.x: MMCSS/network overrides, NTFS/cache overrides, storage LPM overrides, and BCD timer flags.' $confirm=Read-Host ' Create a snapshot and clean them? (y/N)' if ($confirm -notmatch '^[Yy]$') { return } - New-GamingSnapshot -Reason 'Repair v3.x legacy tweaks' | Out-Null - foreach($target in Get-GamingRegistryTargets){ - if($target.Name -in @('AllowAutoGameMode','AutoGameModeEnabled','GameDVR_Enabled','AppCaptureEnabled','HwSchMode','MouseSpeed','MouseThreshold1','MouseThreshold2')){continue} + $targets=@(Get-GamingRegistryTargets | Where-Object { $_.Name -notin @('AllowAutoGameMode','AutoGameModeEnabled','GameDVR_Enabled','AppCaptureEnabled','HwSchMode','MouseSpeed','MouseThreshold1','MouseThreshold2') }) + New-GamingSnapshot -Reason 'Repair v3.x legacy tweaks' -RegistryTargets $targets -IncludeServices -IncludeNetwork -IncludeBcd | Out-Null + foreach($target in $targets){ Remove-RegistryValueIfPresent $target.Path $target.Name } $games='HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks\Games' @@ -548,16 +618,35 @@ function Invoke-LegacyOptimizerRepair { Set-RegistryValueExact $games 'SFIO Priority' 'Normal' String Set-RegistryValueExact $games 'Background Only' 'True' String Remove-RegistryValueIfPresent $games 'Clock Rate' - foreach($adapter in Get-PhysicalNetAdapters){ - Enable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue - Enable-NetAdapterRsc -Name $adapter.Name -Confirm:$false -ErrorAction SilentlyContinue - } - if(Get-Service -Name SysMain -ErrorAction SilentlyContinue){Set-Service -Name SysMain -StartupType Automatic -ErrorAction SilentlyContinue;Start-Service -Name SysMain -ErrorAction SilentlyContinue} - foreach ($name in @('useplatformclock','useplatformtick','disabledynamictick','tscsyncpolicy')) { & "$env:SystemRoot\System32\bcdedit.exe" /deletevalue '{current}' $name 2>$null | Out-Null } + Enable-GamingSupportedOffloads + if(Get-Service -Name SysMain -ErrorAction SilentlyContinue){Set-Service -Name SysMain -StartupType Automatic -ErrorAction Stop;Start-Service -Name SysMain -ErrorAction Stop} + Remove-GamingBcdOverrides Write-ToolStatus OK 'Legacy optimizer overrides removed. DNS, GPU drivers, active power plan, and TCP autotuning were preserved.' Write-ToolStatus INFO 'Reboot recommended.' } +function Enable-GamingSupportedOffloads { + foreach ($adapter in Get-NetworkAdapterStates) { + if ($adapter.RssAvailable) { + Enable-NetAdapterRss -Name $adapter.Name -Confirm:$false -ErrorAction Stop + if (-not (Get-NetAdapterRss -Name $adapter.Name -ErrorAction Stop).Enabled) { throw "RSS enable verification failed: $($adapter.Name)" } + } + if ($adapter.RscAvailable) { + Enable-NetAdapterRsc -Name $adapter.Name -Confirm:$false -ErrorAction Stop + $rsc=Get-NetAdapterRsc -Name $adapter.Name -ErrorAction Stop + if (-not $rsc.IPv4Enabled -or -not $rsc.IPv6Enabled) { throw "RSC enable verification failed: $($adapter.Name)" } + } + } +} + +function Remove-GamingBcdOverrides { + foreach ($state in @(Get-BcdOverrideStates | Where-Object Exists)) { + & "$env:SystemRoot\System32\bcdedit.exe" /deletevalue '{current}' $state.Name 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { throw "BCD removal failed for $($state.Name) (exit $LASTEXITCODE)." } + } + if (@(Get-BcdOverrideStates | Where-Object Exists).Count) { throw 'BCD cleanup verification failed.' } +} + function Resolve-SuiteToolPath { param([string[]]$RelativeCandidates) $suiteRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) diff --git a/pc-gaming-optimizer/PC_Optimizer.ps1 b/pc-gaming-optimizer/PC_Optimizer.ps1 index f2882be..a0cff8c 100644 --- a/pc-gaming-optimizer/PC_Optimizer.ps1 +++ b/pc-gaming-optimizer/PC_Optimizer.ps1 @@ -1,5 +1,11 @@ #requires -version 5.1 +param([switch]$DebugLog,[string]$ExpectedUserSid) +if ($ExpectedUserSid -and $ExpectedUserSid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value) { + Write-Error 'Elevation switched Windows accounts. Launch from an administrator account belonging to the intended user.' + exit 1 +} $ErrorActionPreference='Continue' +if ($DebugLog) { $env:WINDOWS_PC_TOOLKIT_DEBUG='1' } Import-Module (Join-Path $PSScriptRoot 'Modules\Optimizer.Core.psm1') -Force Initialize-GamingToolkit @@ -46,7 +52,7 @@ while ($true) { '4' { Invoke-GameBooster } '5' { Show-GpuAudit } '6' { Show-NetworkAudit; $clean=Read-Host ' Remove old v3.x network tweaks now? (y/N)'; if($clean -match '^[Yy]$'){Invoke-LegacyNetworkCleanup} } - '7' { Show-StorageAudit; $trim=Read-Host ' Run official ReTrim on fixed NTFS volumes? (y/N)'; if($trim -match '^[Yy]$'){Invoke-SafeRetrim} } + '7' { Show-StorageAudit; $trim=Read-Host ' Run official ReTrim on confirmed fixed SSD volumes? (y/N)'; if($trim -match '^[Yy]$'){Invoke-SafeRetrim} } '8' { Show-MemoryAudit } '9' { Show-TimerAudit; $clean=Read-Host ' Remove legacy BCD timer overrides? (y/N)'; if($clean -match '^[Yy]$'){Invoke-LegacyTimerCleanup} } '10' { Show-GpuMsiAudit } diff --git a/pc-gaming-optimizer/README.md b/pc-gaming-optimizer/README.md index 0759352..6709158 100644 --- a/pc-gaming-optimizer/README.md +++ b/pc-gaming-optimizer/README.md @@ -1,4 +1,4 @@ -# PC Gaming Optimizer 5.0 +# PC Gaming Optimizer 5.0.1 Gaming controls, diagnostics and maintenance integrated with the [dashboard](../README.md). Keep the full toolkit extracted together. @@ -13,6 +13,10 @@ Root `START_TOOLKIT.bat` launches one-click optimization/debloat. `Run_All_Optim Automatic optimization preserves power/sleep, SysMain, drivers, HAGS, GPU interrupt mode, timers/BCD, TCP/NIC offloads and shaders. Test HAGS/power changes against the actual game. -Dashboard recovery uses conflict-aware journals in `%ProgramData%\WindowsPCToolkit\Suite\Runs`. Console actions use `GamingOptimizer\Snapshots` and **U** restores the latest gaming snapshot. Legacy snapshots capture a wider settings set: review later manual changes before restoring. Shader deletion cannot be undone. +Dashboard recovery uses conflict-aware journals in `%ProgramData%\WindowsPCToolkit\Suite\Runs`. Console actions use `GamingOptimizer\Snapshots` and **U** restores the latest gaming snapshot after confirmation. New snapshots record the computer/user and capture only that action's registry, service, network or boot settings. Restore checks typed registry values and reports service/NIC/native-tool failures. Legacy snapshots lack identity metadata and can capture a wider settings set: restore only your own local backup and review later manual changes. Console restore can replace later manual edits; use dashboard journals for conflict-aware undo. Shader deletion cannot be undone, and linked cache folders are skipped. + +Console ReTrim requires a positive SSD serial-number match; HDD and unidentified storage are preserved. Boot-configuration query failures stop legacy timer cleanup rather than being interpreted as no overrides. + +Warnings/errors are logged to `%LOCALAPPDATA%\WindowsPCToolkit\Logs\GamingOptimizer.log`, with version and cause. Run `PC_Optimizer.ps1 -DebugLog` or set `WINDOWS_PC_TOOLKIT_DEBUG=1` for detailed startup/configuration/state diagnostics. Debug logging is off by default; diagnostics rotate at 2 MB. Parser/registry checks do not prove performance gains. Compare game frame times and benchmark evidence. diff --git a/pc-gaming-optimizer/Run_As_Admin.bat b/pc-gaming-optimizer/Run_As_Admin.bat index 91d75fc..cf07fd1 100644 --- a/pc-gaming-optimizer/Run_As_Admin.bat +++ b/pc-gaming-optimizer/Run_As_Admin.bat @@ -1,35 +1,21 @@ @echo off -title PC Gaming Optimizer v5.0 -setlocal EnableExtensions +title PC Gaming Optimizer v5.0.1 +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\GamingOptimizer_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\GamingOptimizer_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) - -echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" -"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Optimizer.ps1" +set "TOOLKIT_LAUNCHER=%~f0" +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }; exit 1" +if errorlevel 1 goto :elevate +"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Optimizer.ps1" -ExpectedUserSid "%~1" +set "RC=%ERRORLEVEL%" +goto :finish +:elevate +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 set "RC=%ERRORLEVEL%" +:finish echo [%date% %time%] PC_Optimizer.ps1 exited with %RC% >> "%LOG%" if not "%RC%"=="0" ( - echo. - echo PC Gaming Optimizer exited with code %RC%. Log: %LOG% + echo Toolkit exited with code %RC%. Log: %LOG% pause ) exit /b %RC% diff --git a/pc-gaming-optimizer/Run_VR_Fix.bat b/pc-gaming-optimizer/Run_VR_Fix.bat index 8896dad..acc98cf 100644 --- a/pc-gaming-optimizer/Run_VR_Fix.bat +++ b/pc-gaming-optimizer/Run_VR_Fix.bat @@ -1,35 +1,21 @@ @echo off -title PC VR Fix -setlocal EnableExtensions +title PC VR Readiness Audit v5.0.1 +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\GamingVR_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\GamingVR_elevated.flag" - -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b -) - -echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" -"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0Fix_VR_PCVR.ps1" +set "TOOLKIT_LAUNCHER=%~f0" +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }; exit 1" +if errorlevel 1 goto :elevate +"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0Fix_VR_PCVR.ps1" -ExpectedUserSid "%~1" +set "RC=%ERRORLEVEL%" +goto :finish +:elevate +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 set "RC=%ERRORLEVEL%" +:finish echo [%date% %time%] Fix_VR_PCVR.ps1 exited with %RC% >> "%LOG%" if not "%RC%"=="0" ( - echo. - echo PC Gaming Optimizer exited with code %RC%. Log: %LOG% + echo Toolkit exited with code %RC%. Log: %LOG% pause ) exit /b %RC% diff --git a/pc-privacy-guard/PC_Privacy.ps1 b/pc-privacy-guard/PC_Privacy.ps1 index e83e83f..2b9df34 100644 --- a/pc-privacy-guard/PC_Privacy.ps1 +++ b/pc-privacy-guard/PC_Privacy.ps1 @@ -1,21 +1,35 @@ #requires -version 5.1 <#[ -PC Privacy Guard v2.1 +PC Privacy Guard v2.1.1 Balanced and strict privacy profiles with exact state snapshots. The default profile reduces personalization and activity upload without disabling services needed by Windows Update, Insider builds, games, browsers, or device features. ]#> +param([switch]$DebugLog,[AllowEmptyString()][string]$ExpectedUserSid) $ErrorActionPreference='Stop' -$Version='2.1' +$Version='2.1.1' $StateRoot=Join-Path $env:ProgramData 'WindowsPCToolkit\PrivacyGuard' $SnapshotRoot=Join-Path $StateRoot 'Snapshots' $script:CurrentSnapshot=$null +$PrivacyDebug=($DebugLog -or $env:WINDOWS_PC_TOOLKIT_DEBUG -eq '1') +$PrivacyLog=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\PrivacyGuard.log' +function Write-PrivacyDiagnostic { + param([string]$Kind,[string]$Message) + if (-not $PrivacyDebug -and $Kind -notin @('FAIL','WARN')) { return } + try { + if ($Message.Length -gt 4096) { $Message=$Message.Substring(0,4096) } + [IO.Directory]::CreateDirectory((Split-Path $PrivacyLog -Parent))|Out-Null + [pscustomobject]@{Time=(Get-Date).ToString('o');Version=$Version;Kind=$Kind;Message=$Message} | ConvertTo-Json -Compress | Add-Content -LiteralPath $PrivacyLog -Encoding UTF8 + } catch { Write-Warning "Privacy diagnostic log could not be written: $($_.Exception.Message)" } +} +Write-PrivacyDiagnostic 'DEBUG' "Startup: state=$StateRoot; snapshots=$SnapshotRoot" function Write-Status { param([ValidateSet('OK','INFO','WARN','FAIL','STEP')][string]$Kind,[string]$Message) $color=switch($Kind){'OK'{'Green'}'INFO'{'Cyan'}'WARN'{'Yellow'}'FAIL'{'Red'}default{'White'}} $tag=switch($Kind){'OK'{'[OK]'}'INFO'{'[i]'}'WARN'{'[!!]'}'FAIL'{'[X]'}default{'[>>]'}} Write-Host (" {0} {1}" -f $tag,$Message) -ForegroundColor $color + Write-PrivacyDiagnostic $Kind $Message } function Test-Admin { $p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) @@ -32,16 +46,20 @@ function Get-RegState{ function Set-RegExact{ param([string]$Path,[string]$Name,[object]$Value,[string]$Kind='DWord') if(-not(Test-Path -LiteralPath $Path)){New-Item -Path $Path -Force|Out-Null} - $type=switch($Kind){'String'{'String'}'ExpandString'{'ExpandString'}'Binary'{'Binary'}'DWord'{'DWord'}'MultiString'{'MultiString'}'QWord'{'QWord'}default{'String'}} - $typed=switch($type){'Binary'{[byte[]]@($Value)}'DWord'{[int]$Value}'QWord'{[long]$Value}'MultiString'{[string[]]@($Value)}default{$Value}} + if ($Kind -notin @('String','ExpandString','Binary','DWord','MultiString','QWord')) { throw "Unsupported registry value type: $Kind" } + $type=$Kind + $typed=$Value + switch($type){'Binary'{$typed=[byte[]]@($Value)}'DWord'{$typed=[int]$Value}'QWord'{$typed=[long]$Value}'MultiString'{$typed=[string[]]@($Value)}} New-ItemProperty -LiteralPath $Path -Name $Name -Value $typed -PropertyType $type -Force|Out-Null + $actual=Get-RegState $Path $Name + if (-not $actual.Exists -or $actual.Kind -ne $type -or (ConvertTo-Json -InputObject $actual.Value -Compress) -cne (ConvertTo-Json -InputObject $typed -Compress)) { throw "Registry write read-back failed: $Path\$Name" } } function Restore-RegState{ param([pscustomobject]$State) if($State.Exists){Set-RegExact $State.Path $State.Name $State.Value $State.Kind} elseif((Get-RegState $State.Path $State.Name).Exists){Remove-ItemProperty -LiteralPath $State.Path -Name $State.Name -ErrorAction Stop} $actual=Get-RegState $State.Path $State.Name - if($actual.Exists -ne $State.Exists -or ($actual.Exists -and ($actual.Kind -ne $State.Kind -or (ConvertTo-Json -InputObject $actual.Value -Compress) -ne (ConvertTo-Json -InputObject $State.Value -Compress)))){throw "Registry restore read-back failed: $($State.Path)\$($State.Name)"} + if($actual.Exists -ne $State.Exists -or ($actual.Exists -and ($actual.Kind -ne $State.Kind -or (ConvertTo-Json -InputObject $actual.Value -Compress) -cne (ConvertTo-Json -InputObject $State.Value -Compress)))){throw "Registry restore read-back failed: $($State.Path)\$($State.Name)"} } function Get-PrivacyTargets{ @( @@ -105,14 +123,22 @@ function Get-TaskTargets{ } function New-PrivacySnapshot{ param([string]$Reason='Privacy change') + Write-PrivacyDiagnostic 'DEBUG' "Snapshot requested: $Reason" Initialize-State $regs=foreach($t in Get-PrivacyTargets){Get-RegState $t.P $t.N} - $services=foreach($name in @('DiagTrack','dmwappushservice')){$s=Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $name) -ErrorAction SilentlyContinue;if($s){[pscustomobject]@{Name=$name;StartMode=$s.StartMode;State=$s.State}}} - $tasks=foreach($t in Get-TaskTargets){$task=Get-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue;if($task){[pscustomobject]@{Path=$t.Path;Name=$t.Name;Enabled=($task.State -ne 'Disabled')}}} - $obj=[pscustomobject]@{Schema=1;Version=$Version;Created=(Get-Date).ToString('o');Reason=$Reason;Registry=@($regs);Services=@($services);Tasks=@($tasks)} - $path=Join-Path $SnapshotRoot ("privacy_{0}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss_fff')) + $services=@(Get-CimInstance Win32_Service -Filter "Name='DiagTrack' OR Name='dmwappushservice'" -ErrorAction Stop | ForEach-Object { + if ($_.StartMode -notin @('Auto','Manual','Disabled') -or $_.State -notin @('Running','Stopped')) { throw "Service is not in a restorable state: $($_.Name) ($($_.StartMode), $($_.State)). Retry after it settles." } + [pscustomobject]@{Name=$_.Name;StartMode=$_.StartMode;State=$_.State;DelayedAutoStart=(Get-RegState "HKLM:\SYSTEM\CurrentControlSet\Services\$($_.Name)" 'DelayedAutoStart')} + }) + $availableTasks=@(Get-ScheduledTask -ErrorAction Stop) + $tasks=foreach($t in Get-TaskTargets){$task=$availableTasks | Where-Object { $_.TaskPath -eq $t.Path -and $_.TaskName -eq $t.Name };if($task){[pscustomobject]@{Path=$t.Path;Name=$t.Name;Enabled=[bool]$task.Settings.Enabled}}} + $identity=[Security.Principal.WindowsIdentity]::GetCurrent() + $obj=[pscustomobject]@{Schema=1;Version=$Version;Computer=$env:COMPUTERNAME;User=$identity.Name;UserSid=$identity.User.Value;Created=(Get-Date).ToString('o');Reason=$Reason;Registry=@($regs);Services=@($services);Tasks=@($tasks)} + $path=Join-Path $SnapshotRoot ("privacy_{0}_{1}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss_fff'),[guid]::NewGuid().ToString('N')) $obj|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path -Encoding UTF8 - Set-Content -LiteralPath (Join-Path $StateRoot 'latest_snapshot.txt') -Value $path -Encoding ASCII + $verified=Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json + if ((ConvertTo-Json -InputObject $verified -Depth 8 -Compress) -ne (ConvertTo-Json -InputObject $obj -Depth 8 -Compress)) { throw 'Privacy backup verification failed; no privacy settings were changed.' } + Set-Content -LiteralPath (Join-Path $StateRoot 'latest_snapshot.txt') -Value $path -Encoding UTF8 $script:CurrentSnapshot=$path Write-Status OK ("Safety snapshot saved: {0}" -f $path) return $path @@ -124,13 +150,22 @@ function Get-LatestSnapshot{ } function Restore-LatestSnapshot{ Assert-Admin;Initialize-State;$path=Get-LatestSnapshot;if(-not $path){Write-Status WARN 'No Privacy Guard snapshot exists.';return} - $snap=Get-Content -LiteralPath $path -Raw|ConvertFrom-Json - if($snap.Computer -ne $env:COMPUTERNAME -or $snap.User -ne [Security.Principal.WindowsIdentity]::GetCurrent().Name){throw 'This privacy snapshot belongs to another computer or user.'} + $snap=Get-Content -LiteralPath $path -Raw -Encoding UTF8|ConvertFrom-Json + if ($snap.PSObject.Properties.Name -contains 'Schema' -and $snap.Schema -ne 1) { throw 'Unsupported privacy backup schema.' } + $identity=[Security.Principal.WindowsIdentity]::GetCurrent() + if ($snap.PSObject.Properties.Name -notcontains 'Computer' -or $snap.PSObject.Properties.Name -notcontains 'User') { + Write-Status WARN 'This legacy privacy backup did not record its computer or user. Its origin cannot be verified.' + if ((Read-Host ' Restore this legacy backup to the current computer and user? (y/N)') -notmatch '^[Yy]$') { throw 'Legacy privacy restore cancelled; no settings were changed.' } + } elseif ($snap.Computer -ne $env:COMPUTERNAME -or ($snap.PSObject.Properties.Name -contains 'UserSid' -and $snap.UserSid -ne $identity.User.Value) -or ($snap.PSObject.Properties.Name -notcontains 'UserSid' -and $snap.User -ne $identity.Name)) { throw 'This privacy snapshot belongs to another computer or user.' } + foreach($s in @($snap.Services)) { + if ($s.StartMode -notin @('Auto','Manual','Disabled') -or $s.State -notin @('Running','Stopped')) { throw "Unsupported saved service state: $($s.Name). No settings were changed." } + } foreach($s in @($snap.Registry)){Restore-RegState $s} foreach($s in @($snap.Services)){ $type=switch($s.StartMode){'Auto'{'Automatic'}'Manual'{'Manual'}'Disabled'{'Disabled'}default{$null}} if(-not $type){throw "Unsupported saved service mode: $($s.Name)"} Set-Service -Name $s.Name -StartupType $type -ErrorAction Stop + if ($s.PSObject.Properties.Name -contains 'DelayedAutoStart') { Restore-RegState $s.DelayedAutoStart } if($s.State -eq 'Running'){Start-Service -Name $s.Name -ErrorAction Stop}elseif($s.State -eq 'Stopped'){Stop-Service -Name $s.Name -Force -ErrorAction Stop} $actual=Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $s.Name) -ErrorAction Stop if($actual.StartMode -ne $s.StartMode -or $actual.State -ne $s.State){throw "Service restore read-back failed: $($s.Name)"} @@ -207,8 +242,20 @@ function Apply-CloudSearchPrivacy{ } function Apply-StrictTelemetryServiceProfile{ Ensure-Snapshot 'Strict telemetry service profile' - foreach($name in @('DiagTrack','dmwappushservice')){if(Get-Service -Name $name -ErrorAction SilentlyContinue){Stop-Service -Name $name -Force -ErrorAction SilentlyContinue;Set-Service -Name $name -StartupType Disabled -ErrorAction SilentlyContinue;Write-Status OK ("Disabled service: {0}" -f $name)}} - foreach($t in Get-TaskTargets){if(Get-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue){Disable-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue|Out-Null;Write-Status OK ("Disabled task: {0}{1}" -f $t.Path,$t.Name)}} + $snapshot=Get-Content -LiteralPath $script:CurrentSnapshot -Raw -Encoding UTF8 | ConvertFrom-Json + foreach($saved in @($snapshot.Services)){ + $name=$saved.Name + Stop-Service -Name $name -Force -ErrorAction Stop + Set-Service -Name $name -StartupType Disabled -ErrorAction Stop + $actual=Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $name) -ErrorAction Stop + if (-not $actual -or $actual.StartMode -ne 'Disabled' -or $actual.State -ne 'Stopped') { throw "Service disable read-back failed: $name" } + Write-Status OK ("Disabled service: {0}" -f $name) + } + foreach($t in @($snapshot.Tasks)){ + Disable-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction Stop|Out-Null + if ([bool](Get-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction Stop).Settings.Enabled) { throw "Task disable read-back failed: $($t.Name)" } + Write-Status OK ("Disabled task: {0}{1}" -f $t.Path,$t.Name) + } Write-Status WARN 'Strict mode can reduce diagnostic data available to Microsoft support and compatibility analysis.' Write-Status INFO 'Windows Update, BITS, Delivery Optimization, DNS Client, Defender, Store, Xbox, audio, GPU, camera, microphone, and Windows Insider Service were not disabled.' } @@ -282,9 +329,12 @@ function Show-Menu{ Write-Host '' } -Assert-Admin;Initialize-State +try { + if ($ExpectedUserSid -and [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -ne $ExpectedUserSid) { throw 'Privacy Guard was elevated as a different Windows user. Run with administrator rights for your own account; no privacy settings were changed.' } + Assert-Admin;Initialize-State +} catch { Write-PrivacyDiagnostic 'FAIL' $_.Exception.Message; throw } while($true){ - $script:CurrentSnapshot=$null;Show-Header;Show-Dashboard;Show-Menu;$c=(Read-Host ' Select').Trim().ToUpperInvariant() + $script:CurrentSnapshot=$null;Show-Header;Show-Dashboard;Show-Menu;$c=(Read-Host ' Select').Trim().ToUpperInvariant();Write-PrivacyDiagnostic 'DEBUG' "Selected action: $c" try{switch($c){'A'{Apply-BalancedProfile}'S'{Apply-StrictProfile}'1'{New-PrivacySnapshot 'Personalization privacy'|Out-Null;Apply-PersonalizationPrivacy}'2'{New-PrivacySnapshot 'Activity privacy'|Out-Null;Apply-ActivityPrivacy}'3'{New-PrivacySnapshot 'Required diagnostics policy'|Out-Null;Apply-RequiredDiagnostics}'4'{New-PrivacySnapshot 'Browser telemetry policies'|Out-Null;Apply-BrowserTelemetryPolicies}'5'{New-PrivacySnapshot 'Location privacy'|Out-Null;Apply-LocationPrivacy}'6'{New-PrivacySnapshot 'Cloud search privacy'|Out-Null;Apply-CloudSearchPrivacy}'7'{New-PrivacySnapshot 'Delivery Optimization privacy'|Out-Null;Apply-DeliveryOptimizationPrivacy}'8'{New-PrivacySnapshot 'Strict telemetry services'|Out-Null;Apply-StrictTelemetryServiceProfile}'D'{Open-DnsManager}'U'{Restore-LatestSnapshot}'0'{break}default{Write-Status WARN 'Invalid selection.'}}}catch{Write-Status FAIL $_.Exception.Message} if($c -eq '0'){break};Write-Host '';Read-Host ' Press Enter to continue'|Out-Null } diff --git a/pc-privacy-guard/README.md b/pc-privacy-guard/README.md index 6233f74..01b5d87 100644 --- a/pc-privacy-guard/README.md +++ b/pc-privacy-guard/README.md @@ -1,4 +1,4 @@ -# PC Privacy Guard v2.1 +# PC Privacy Guard v2.1.1 A reversible Windows 10/11 privacy tool with two clearly separated profiles. @@ -25,6 +25,8 @@ Before any profile or individual action, the app records the exact registry valu Undo checks the snapshot's computer/user, restores those values and verifies registry types, service states and task flags. Restore failures stop the success message and show the actual error. It does not delete whole Edge, Chrome, Firefox, or Windows policy branches, so unrelated settings remain intact. +Version 2.1.1 records the identity fields that earlier snapshots omitted, verifies the backup before changes, and preserves delayed automatic service startup. Legacy backups with no recorded identity require explicit confirmation before restoring. Failed service/task inventory aborts the backup; denied strict-profile writes and failed read-back checks report failure. + ## Encrypted DNS Use the dashboard or root `dns-encrypted-doh\DNS_Manager.ps1` for Windows 11 DNS-over-HTTPS. DoH encrypts DNS lookups on the network path. It does not change your public IP; a VPN is the tool that changes the address websites see. @@ -33,4 +35,8 @@ Use the dashboard or root `dns-encrypted-doh\DNS_Manager.ps1` for Windows 11 DNS Double-click `Run_As_Admin.bat`, accept UAC, and choose Balanced or individual controls. Reboot after Strict mode. +The launcher carries your original account SID through elevation. If UAC selects a different administrator account, Privacy Guard stops before initializing state or changing that account's HKCU settings. + In 2.1, unreadable registry state aborts snapshot creation rather than recording a missing value and risking an incorrect undo. Keep the full toolkit extracted together. + +Errors are recorded in `%LOCALAPPDATA%\WindowsPCToolkit\Logs\PrivacyGuard.log`. Add `-DebugLog`, or set `WINDOWS_PC_TOOLKIT_DEBUG=1` before launching, to include startup version/configuration and operation events. Detailed logging is off by default. diff --git a/pc-privacy-guard/Run_As_Admin.bat b/pc-privacy-guard/Run_As_Admin.bat index 0cde61d..32a0e8c 100644 --- a/pc-privacy-guard/Run_As_Admin.bat +++ b/pc-privacy-guard/Run_As_Admin.bat @@ -1,35 +1,28 @@ @echo off -title PC Privacy Guard v2.1 -setlocal EnableExtensions +title PC Privacy Guard v2.1.1 +setlocal EnableExtensions DisableDelayedExpansion set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" set "LOG=%LOCALAPPDATA%\Temp\PrivacyGuard_launch.log" -set "FLAG=%LOCALAPPDATA%\Temp\PrivacyGuard_elevated.flag" +"%PS%" -NoProfile -Command "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }" +if errorlevel 1 goto Elevate -net session >nul 2>&1 -if errorlevel 1 ( - echo [%date% %time%] not elevated - requesting administrator rights >> "%LOG%" - del "%FLAG%" >nul 2>&1 - "%PS%" -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" >> "%LOG%" 2>&1 - "%SystemRoot%\System32\ping.exe" -n 7 127.0.0.1 >nul - if not exist "%FLAG%" ( - echo [%date% %time%] elevation did not complete >> "%LOG%" - echo. - echo Elevation did not complete - no administrator window appeared. - echo Right-click this .bat and choose "Run as administrator". - echo Log: %LOG% - pause - ) - exit /b +echo [%date% %time%] elevated start >> "%LOG%" +"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Privacy.ps1" -ExpectedUserSid "%~1" +set "RC=%ERRORLEVEL%" +echo [%date% %time%] toolkit exited with %RC% >> "%LOG%" +if not "%RC%"=="0" ( + echo Toolkit exited with code %RC%. Log: %LOG% + pause ) +exit /b %RC% -echo [%date% %time%] elevated start >> "%LOG%" -echo elevated > "%FLAG%" -"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0PC_Privacy.ps1" +:Elevate +set "TOOLKIT_LAUNCHER=%~f0" +echo [%date% %time%] requesting administrator rights >> "%LOG%" +"%PS%" -NoProfile -Command "try { $p=Start-Process -FilePath $env:TOOLKIT_LAUNCHER -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) -Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $p.ExitCode } catch { Write-Error $_; exit 1 }" >> "%LOG%" 2>&1 set "RC=%ERRORLEVEL%" -echo [%date% %time%] PC_Privacy.ps1 exited with %RC% >> "%LOG%" if not "%RC%"=="0" ( - echo. - echo PC Privacy Guard exited with code %RC%. Log: %LOG% + echo Elevation or the toolkit failed. Log: %LOG% pause ) exit /b %RC% diff --git a/tests/Test_All.ps1 b/tests/Test_All.ps1 new file mode 100644 index 0000000..62a6731 --- /dev/null +++ b/tests/Test_All.ps1 @@ -0,0 +1,6 @@ +#requires -version 5.1 +$ErrorActionPreference='Stop' +foreach ($test in @('Test_Toolkit.ps1','Test_AuditSuite.ps1','Test_AuditCleaner.ps1','Test_AuditDnsPrivacy.ps1','Test_AuditRepairGaming.ps1','Test_AuditLaunchers.ps1')) { + & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot $test) + if ($LASTEXITCODE -ne 0) { throw "$test failed with exit $LASTEXITCODE" } +} diff --git a/tests/Test_AuditCleaner.ps1 b/tests/Test_AuditCleaner.ps1 new file mode 100644 index 0000000..e9183f8 --- /dev/null +++ b/tests/Test_AuditCleaner.ps1 @@ -0,0 +1,177 @@ +#requires -version 5.1 +param([string]$CleanerPath) +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +if (-not $CleanerPath) { + $CleanerPath=Join-Path $root 'pc-cleaner\PC_Cleaner.ps1' + if (-not (Test-Path -LiteralPath $CleanerPath)) { $CleanerPath=Join-Path $root 'Pc Cleaner\PC_Cleaner.ps1' } +} +$fixture=Join-Path $env:TEMP ('Toolkit_CleanerAudit_'+[guid]::NewGuid().ToString('N')) +$key='HKCU:\Software\WindowsPCToolkitTests\Cleaner_'+[guid]::NewGuid().ToString('N') +$checks=0 +function Check([bool]$Condition,[string]$Message) { if (-not $Condition) { throw "TEST FAILED: $Message" }; $script:checks++ } +function Write-OldFile([string]$Path) { + [void][IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) + [IO.File]::WriteAllText($Path,'owned fixture') + [IO.File]::SetLastWriteTime($Path,(Get-Date).AddDays(-10)) + [IO.File]::SetLastAccessTime($Path,(Get-Date).AddDays(-10)) +} +[void][IO.Directory]::CreateDirectory($fixture) +$links=New-Object Collections.ArrayList +try { + . $CleanerPath -Action Library + $denied=$false + try { . $CleanerPath -Action Library -ExpectedUserSid 'S-1-5-21-0-0-0-999999' } catch { $denied=$true } + Check $denied 'Cleaner refuses a different Windows identity before any bootstrap or action' + . $CleanerPath -Action Library -ExpectedUserSid ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value) + Check $true 'Own Windows identity can load the cleaner without side effects' + Check ((Get-CommandTargetPath '"C:\App Dir\app.exe" --startup') -eq 'C:\App Dir\app.exe') 'Quoted executable path is parsed exactly' + Check ($null -eq (Get-CommandTargetPath 'C:\App Dir\app.exe --startup')) 'Ambiguous unquoted path with spaces is retained for review' + Check ($null -eq (Get-CommandTargetPath 'C:\Dir.exe name\app.exe')) 'Executable-looking directory prefix does not become a false dead target' + Check ((Get-CommandTargetPath 'C:\Apps\app.exe --startup') -eq 'C:\Apps\app.exe') 'Unambiguous executable with arguments remains supported' + $brackets=Join-Path $fixture "Cache [1] ' literal" + Write-OldFile (Join-Path $brackets 'Default\Cache\old.tmp') + $pattern=[pscustomobject]@{Paths=@((Join-Path $brackets '*\Cache'))} + Check (@(Resolve-CategoryDirs $pattern).Count -eq 1) 'Wildcard categories preserve literal brackets and apostrophes in parent paths' + $pattern.Paths=@((Join-Path $brackets 'Default\Cache\*.tmp')) + Check (@(Resolve-CategoryFiles $pattern).Count -eq 1) 'Wildcard file categories preserve literal brackets and apostrophes' + $cats=@(Get-CleanCategories) + foreach ($id in @('WindowsOld','BakFiles','WUCache','RecallData','AiAppCaches')) { + $category=$cats | Where-Object Id -eq $id + Check ($category.Profile -eq 'Report') "$id is excluded from both deleting profiles" + # Even a caller with a stale Strict category cannot revive removed deletion code. + $category.Profile='Strict'; $category.Kind='Special' + $result=Invoke-CleanCategory $category + Check ($result.Status -eq 'Skipped' -and $result.Removed -eq 0) "$id executor refuses a stale destructive category" + } + $tempRoot=Join-Path $fixture 'Temp' + $outside=Join-Path $fixture 'Outside' + Write-OldFile (Join-Path $tempRoot 'old.tmp') + Write-OldFile (Join-Path $tempRoot 'new.tmp') + [IO.File]::SetLastWriteTime((Join-Path $tempRoot 'new.tmp'),(Get-Date)) + Write-OldFile (Join-Path $tempRoot 'accessed.tmp') + [IO.File]::SetLastAccessTime((Join-Path $tempRoot 'accessed.tmp'),(Get-Date)) + Write-OldFile (Join-Path $tempRoot 'Documents\keep.tmp') + Write-OldFile (Join-Path $tempRoot '.ollama\models\keep.tmp') + Write-OldFile (Join-Path $outside 'Cache\keep.tmp') + $link=Join-Path $fixture 'linked' + New-Item -ItemType Junction -Path $link -Value $outside | Out-Null + [void]$links.Add($link) + $nested=Join-Path $tempRoot 'nested-link' + New-Item -ItemType Junction -Path $nested -Value $outside | Out-Null + [void]$links.Add($nested) + Check (-not (Test-CleanPathWithoutLinks (Join-Path $link 'Cache\keep.tmp'))) 'Ancestor junction is refused, even when leaf is a normal file' + Check (-not (Remove-ItemSafe (Join-Path $link 'Cache\keep.tmp'))) 'Direct file deletion through an ancestor junction is refused' + Check (-not (Remove-ItemSafe (Join-Path $link 'Cache') -Directory)) 'Recursive directory deletion through an ancestor junction is refused' + Check (@(Get-DisposableTempFiles @((Join-Path $link 'Cache'))).Count -eq 0) 'Temp root below a junction yields no candidates' + $category=[pscustomobject]@{Id='EdgeCaches';Label='Fixture';Profile='Balanced';Kind='Children';Paths=@((Join-Path $link 'Cache'))} + $result=Invoke-CleanCategory $category + Check ($result.Removed -eq 0 -and (Test-Path -LiteralPath (Join-Path $outside 'Cache\keep.tmp'))) 'Category cache root below a junction preserves destination files' + $category=[pscustomobject]@{Id='UserTemp';Label='Fixture';Profile='Balanced';Kind='Children';Paths=@($tempRoot)} + $measurement=Measure-Category $category + Check ($measurement.Files -eq 1) 'Temp scan counts only aged, unaccessed, unprotected files and skips junctions' + # Enumeration may update access time; reset only the one owned candidate. + [IO.File]::SetLastAccessTime((Join-Path $tempRoot 'old.tmp'),(Get-Date).AddDays(-10)) + $result=Invoke-CleanCategory $category + Check ($result.Removed -eq 1 -and -not (Test-Path -LiteralPath (Join-Path $tempRoot 'old.tmp'))) 'Aged disposable file is actually deleted' + foreach ($path in @('new.tmp','accessed.tmp','Documents\keep.tmp','.ollama\models\keep.tmp')) { Check (Test-Path -LiteralPath (Join-Path $tempRoot $path)) "Protected/recent $path survives" } + $locked=Join-Path $tempRoot 'locked.tmp' + Write-OldFile $locked + $stream=[IO.File]::Open($locked,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None) + try { Check (-not (Remove-ItemSafe $locked)) 'Locked file removal returns a skip, preserving bytes' } finally { $stream.Dispose() } + Check (Test-Path -LiteralPath $locked) 'Locked file remains after failed removal' + Check (-not (Remove-ItemSafe $tempRoot -Directory)) 'Recursive deletion containing protected names or links is refused' + $savedTemp=(Get-Item Function:Get-DisposableTempFiles).ScriptBlock + function Get-DisposableTempFiles { param($Roots) [IO.File]::SetLastWriteTime($locked,(Get-Date)); Get-Item -LiteralPath $locked } + $result=Invoke-CleanCategory $category + Check ($result.Removed -eq 0 -and (Test-Path -LiteralPath $locked)) 'Temp cleanup rechecks file age after enumeration' + Set-Item Function:Get-DisposableTempFiles $savedTemp + + $shown=New-Object Collections.ArrayList + function Show-ScanLine { param($Category,$Info,$Prefix) [void]$shown.Add($Category.Id) } + function Measure-Category { param($Category) [pscustomobject]@{Files=1;Bytes=[long]1} } + $null=Show-ScanReport + foreach ($id in @('WindowsOld','BakFiles','WUCache','RecallData','AiAppCaches','ModelStores')) { Check ($shown -contains $id) "Scan renders the kept $id category" } + Remove-Item Function:Show-ScanLine,Function:Measure-Category + + # Official cmdlet is faked: never clean the real Windows delivery cache. + function Measure-Category { param($Category) [pscustomobject]@{Files=5;Bytes=[long]1234} } + function Delete-DeliveryOptimizationCache { param([switch]$Force) } + $result=Invoke-CleanCategory ([pscustomobject]@{Id='DeliveryOptimization';Label='Fixture';Profile='Balanced';Kind='Special';Paths=@()}) + Check ($result.Removed -eq 0 -and $result.Bytes -eq 0) 'No-op Windows cache cleanup does not claim every pre-existing file was deleted' + function Delete-DeliveryOptimizationCache { param([switch]$Force) throw 'injected permission failure' } + $result=Invoke-CleanCategory ([pscustomobject]@{Id='DeliveryOptimization';Label='Fixture';Profile='Balanced';Kind='Special';Paths=@()}) + Check ($result.Status -eq 'Failed' -and $result.Note -match 'injected permission failure') 'Cache command failure remains a failure in the result' + Remove-Item Function:Measure-Category,Function:Delete-DeliveryOptimizationCache + + # One real typed journal against an owned HKCU key; no real startup key is touched. + Import-Module (Join-Path $root 'Modules\Toolkit.Core.psm1') -ArgumentList (Join-Path $fixture 'State') -Force -DisableNameChecking + & (Get-Module Toolkit.Core) { function script:Assert-Administrator { } } + New-Item -Path $key -Force | Out-Null + New-ItemProperty -LiteralPath $key -Name 'Fixture' -Value ('"'+(Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe')+'"') -PropertyType String | Out-Null + function Assert-Admin { } + function Import-Module { } + function Read-Host { param($Prompt) 'A' } + function Get-RegistryIssues { [pscustomobject]@{Category='Dead startup entries';Kind='Value';Key=$key;Name='Fixture';Display=($fixture+'\missing.exe');Evidence='old scan'} } + Invoke-RegistryCare + Check ((Get-Item -LiteralPath $key).GetValueNames() -contains 'Fixture') 'Registry care rechecks current startup command and preserves a newly valid target' + Set-ItemProperty -LiteralPath $key -Name 'Fixture' -Value ('"'+$fixture+'\missing.exe"') + Invoke-RegistryCare + Check ((Get-Item -LiteralPath $key).GetValueNames() -notcontains 'Fixture') 'Registry care removes only the currently verified missing target' + $history=Get-SuiteHistory | Where-Object Mode -eq 'RegistryCare' | Select-Object -First 1 + $undo=Invoke-SuiteUndo $history.Path + Check ($undo.Status -eq 'Restored' -and (Get-ItemPropertyValue -LiteralPath $key -Name 'Fixture') -eq ('"'+$fixture+'\missing.exe"')) 'Cleaner registry journal undo restores exact original value' + $suite=Get-Module Toolkit.Core + function Set-SuiteRegistry { + param($Run,$Path,$Name,$Value,[switch]$Remove) + & $suite { param($Run,$Path,$Name,$Value) Set-SuiteRegistry $Run $Path $Name $Value -Remove } $Run $Path $Name $Value + throw 'injected interruption after removal' + } + Invoke-RegistryCare + Check ((Get-ItemPropertyValue -LiteralPath $key -Name 'Fixture') -eq ('"'+$fixture+'\missing.exe"')) 'Failed individual registry repair rolls back its partial removal' + $failed=Get-SuiteHistory | Where-Object Mode -eq 'RegistryCare' | Select-Object -First 1 + Check ($failed.Status -eq 'CompletedWithErrors') 'Partial registry repair failure remains visible after rollback' + Remove-Item Function:Set-SuiteRegistry + # Legacy file backup restore must not overwrite a replacement made by the user. + $RegBackupRoot=Join-Path $fixture 'Legacy' + $legacy=Join-Path $RegBackupRoot '20200101' + [void][IO.Directory]::CreateDirectory($legacy) + $existing=Join-Path $fixture 'replacement.lnk'; $backup=Join-Path $legacy 'original.lnk' + [IO.File]::WriteAllText($existing,'new user shortcut'); [IO.File]::WriteAllText($backup,'old shortcut') + [pscustomobject]@{Items=@([pscustomobject]@{Type='File';Path=$existing;Backup=$backup})} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $legacy 'manifest.json') -Encoding UTF8 + function Get-SuiteHistory { } + Restore-LatestRegistryBackup + Check ([IO.File]::ReadAllText($existing) -eq 'new user shortcut') 'Legacy file restore preserves an existing replacement' + Remove-Item Function:Get-SuiteHistory + function Read-Host { param($Prompt) '1-2147483647' } + Invoke-PickCategories + Check $true 'Unbounded category range is rejected without allocation or deletion' + foreach ($name in @('Assert-Admin','Import-Module','Read-Host','Get-RegistryIssues')) { Remove-Item ('Function:'+$name) } + # Restore cleaner helpers after scope-local fakes and module imports. + . $CleanerPath -Action Library + $StateRoot=Join-Path $fixture 'CleanerState'; $LogRoot=Join-Path $StateRoot 'Logs'; $RegBackupRoot=Join-Path $StateRoot 'RegistryBackups'; $FileBackupRoot=Join-Path $StateRoot 'FileBackups' + $sample=[pscustomobject]@{Bytes=0;Status='Failed'} + $first=Save-RunLog 'Fixture' @($sample) 0 0 + $second=Save-RunLog 'Fixture' @($sample) 0 0 + Check ($first -ne $second -and (Test-Path -LiteralPath $first) -and (Test-Path -LiteralPath $second)) 'Runs in the same second retain separate logs' + Check ((Get-Content -LiteralPath $first -Raw | ConvertFrom-Json).Status -eq 'CompletedWithErrors') 'Persisted clean log reports category failure' + $savedLocal=$env:LOCALAPPDATA + try { + $env:LOCALAPPDATA=Join-Path $fixture 'Local' + Write-CleanerDiagnostic 'Fixture' 'quiet' + Check (-not (Test-Path -LiteralPath (Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\Cleaner_debug.log'))) 'Library import stays quiet with no debug files' + $script:CleanerDebugEnabled=$true + Write-CleanerDiagnostic 'Startup' "Action=Fixture; StateRoot=$StateRoot" + $debug=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\Cleaner_debug.log' + Check ((Get-Content -LiteralPath $debug -Raw) -match 'v1\.1\.1 Startup: Action=Fixture; StateRoot=') 'Enabled diagnostic records version and resolved configuration' + [IO.File]::WriteAllBytes($debug,(New-Object byte[] 1048576)) + Write-CleanerDiagnostic 'Failure' 'fixture cause' + Check ((Test-Path -LiteralPath ($debug+'.1')) -and (Get-Item -LiteralPath $debug).Length -lt 1MB) 'Diagnostic output rotates at one MiB' + } finally { $env:LOCALAPPDATA=$savedLocal } + Write-Host "PASS: $checks cleaner audit regression checks" +} finally { + foreach ($link in $links) { if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } } + if ($stream) { $stream.Dispose() } + Remove-Item -LiteralPath $key -Recurse -Force -ErrorAction SilentlyContinue + if ([IO.Path]::GetFileName($fixture) -like 'Toolkit_CleanerAudit_*') { Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/tests/Test_AuditDnsPrivacy.ps1 b/tests/Test_AuditDnsPrivacy.ps1 new file mode 100644 index 0000000..fec6a1b --- /dev/null +++ b/tests/Test_AuditDnsPrivacy.ps1 @@ -0,0 +1,177 @@ +#requires -version 5.1 +param([string]$Root=(Split-Path $PSScriptRoot -Parent)) +$ErrorActionPreference='Stop' +$script:Checks=0 +function Check([bool]$Condition,[string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:Checks++; Write-Host "PASS: $Message" +} +function Expect-Failure([scriptblock]$Action,[string]$Pattern,[string]$Message) { + $failure=$null; try { & $Action | Out-Null } catch { $failure=$_.Exception.Message } + Check ($null -ne $failure -and $failure -like $Pattern) $Message +} +$fixture=Join-Path ([IO.Path]::GetTempPath()) ('Toolkit-DnsPrivacy-'+[guid]::NewGuid().ToString('N')) +$key='HKCU:\Software\WindowsPCToolkitAudit\'+[guid]::NewGuid().ToString('N') +[IO.Directory]::CreateDirectory($fixture)|Out-Null +try { + & { + $privacy=Join-Path $Root 'pc-privacy-guard\PC_Privacy.ps1' + if (-not (Test-Path -LiteralPath $privacy)) { $privacy=Join-Path $Root 'Pc Privacy Guard\PC_Privacy.ps1' } + $ast=[Management.Automation.Language.Parser]::ParseFile($privacy,[ref]$null,[ref]$null) + Invoke-Expression ((@($ast.FindAll({param($n) $n -is [Management.Automation.Language.FunctionDefinitionAst]},$false)) | ForEach-Object { $_.Extent.Text }) -join "`n") + $Version='2.1.1'; $StateRoot=Join-Path $fixture 'Privacy'; $SnapshotRoot=Join-Path $StateRoot 'Snapshots' + $PrivacyLog=Join-Path $fixture 'Logs\Privacy.log'; $PrivacyDebug=$false + Write-PrivacyDiagnostic 'INFO' 'quiet-event' + Check (-not (Test-Path -LiteralPath $PrivacyLog)) 'Privacy detailed events stay quiet by default' + Write-PrivacyDiagnostic 'FAIL' ('f'*5000) + $entry=Get-Content -LiteralPath $PrivacyLog -Raw -Encoding UTF8 | ConvertFrom-Json + Check ($entry.Version -eq $Version -and $entry.Kind -eq 'FAIL' -and $entry.Message.Length -eq 4096) 'Privacy failures retain version and bounded cause while detailed logging is off' + $PrivacyDebug=$true; Write-PrivacyDiagnostic 'DEBUG' 'debug-event'; $PrivacyDebug=$false + Check (@(Get-Content -LiteralPath $PrivacyLog -Encoding UTF8).Count -eq 2) 'Privacy debug logging can be toggled on' + $script:CurrentSnapshot=$null + function Assert-Admin { } + function Write-Status { param($Kind,$Message) if ($Kind -eq 'OK') { $script:SuccessMessages++ } } + function Get-PrivacyTargets { @{P=$key;N='Fixture'} } + function Get-TaskTargets { @{Path='\Fixture\';Name='FixtureTask'} } + function Get-CimInstance { param($ClassName,$Filter) if ($script:FailInventory) { throw 'Injected service inventory failure' }; @() } + function Get-ScheduledTask { param($TaskPath,$TaskName) if ($script:FailTasks) { throw 'Injected task inventory failure' }; @() } + function Stop-Service { param($Name,[switch]$Force) throw 'Injected denied service stop' } + function Start-Service { throw 'Unexpected service mutation' } + function Set-Service { throw 'Unexpected service mutation' } + function Disable-ScheduledTask { throw 'Injected denied task disable' } + function Enable-ScheduledTask { throw 'Unexpected task mutation' } + function Read-Host { 'n' } + Check ((Get-Command Stop-Service).CommandType -eq 'Function' -and (Get-Command Set-Service).CommandType -eq 'Function') 'All privacy service mutations are intercepted before testing' + Set-RegExact $key 'Fixture' '%TEMP%\literal' ExpandString + foreach ($typedCase in @(@{Kind='Binary';Value=[byte[]]@(42)},@{Kind='Binary';Value=[byte[]]@()},@{Kind='MultiString';Value=[string[]]@('one')},@{Kind='MultiString';Value=[string[]]@()})) { + Set-RegExact $key 'TypedFixture' $typedCase.Value $typedCase.Kind + $before=Get-RegState $key 'TypedFixture' + Set-RegExact $key 'TypedFixture' 'changed' String + Restore-RegState ($before | ConvertTo-Json | ConvertFrom-Json) + $actual=Get-RegState $key 'TypedFixture' + Check ($actual.Kind -eq $typedCase.Kind -and (ConvertTo-Json -InputObject $actual.Value -Compress) -eq (ConvertTo-Json -InputObject $typedCase.Value -Compress)) "Privacy typed restore preserves $($typedCase.Kind) arrays of length $($typedCase.Value.Count)" + } + $path=New-PrivacySnapshot 'Disposable registry round-trip' + $snapshot=Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json + Check ($snapshot.Computer -eq $env:COMPUTERNAME -and $snapshot.User -eq [Security.Principal.WindowsIdentity]::GetCurrent().Name -and $snapshot.UserSid -eq [Security.Principal.WindowsIdentity]::GetCurrent().User.Value) 'Fresh privacy backup records the identity required by undo' + Set-RegExact $key 'Fixture' 'changed' String + Restore-LatestSnapshot + $actual=Get-RegState $key 'Fixture' + Check ($actual.Kind -eq 'ExpandString' -and $actual.Value -eq '%TEMP%\literal') 'Fresh privacy snapshot restores exact disposable HKCU type and literal value' + $script:FailInventory=$true + Expect-Failure { New-PrivacySnapshot } '*Injected service inventory failure*' 'Service inventory failures abort privacy backups' + $script:FailInventory=$false; $script:FailTasks=$true + Expect-Failure { New-PrivacySnapshot } '*Injected task inventory failure*' 'Task inventory failures abort privacy backups' + $script:FailTasks=$false + function Get-CimInstance { param($ClassName,$Filter) [pscustomobject]@{Name='FixtureService';StartMode='Auto';State='Stopped'} } + function Get-ScheduledTask { param($TaskPath,$TaskName) [pscustomobject]@{TaskPath='\Fixture\';TaskName='FixtureTask';State='Ready';Settings=[pscustomobject]@{Enabled=$false}} } + $capturedPath=New-PrivacySnapshot 'Capture service and task flags' + $captured=Get-Content -LiteralPath $capturedPath -Raw -Encoding UTF8 | ConvertFrom-Json + Check ($captured.Services[0].DelayedAutoStart.Name -eq 'DelayedAutoStart' -and -not $captured.Tasks[0].Enabled) 'Service backup includes delayed startup and task backup uses Settings.Enabled' + function Get-CimInstance { param($ClassName,$Filter) [pscustomobject]@{Name='FixtureService';StartMode='Auto';State='Start Pending'} } + Expect-Failure { New-PrivacySnapshot } '*not in a restorable state*' 'Transient service state aborts snapshots before privacy changes' + Set-Content -LiteralPath (Join-Path $StateRoot 'latest_snapshot.txt') -Value $path -Encoding UTF8 + $snapshot.UserSid='S-1-5-21-0-0-0-99999'; $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + Set-RegExact $key 'Fixture' 'foreign-guard' String + Expect-Failure { Restore-LatestSnapshot } '*another computer or user*' 'Foreign-user backups fail before registry mutation' + Check ((Get-RegState $key 'Fixture').Value -eq 'foreign-guard') 'Foreign-user preflight preserves registry state' + $snapshot.PSObject.Properties.Remove('Computer'); $snapshot.PSObject.Properties.Remove('User'); $snapshot.PSObject.Properties.Remove('UserSid') + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + Expect-Failure { Restore-LatestSnapshot } '*Legacy privacy restore cancelled*' 'Legacy identity-less backup requires explicit confirmation' + function Read-Host { 'y' } + Restore-LatestSnapshot + Check ((Get-RegState $key 'Fixture').Value -eq '%TEMP%\literal') 'Confirmed legacy backup remains recoverable' + $snapshot.Services=@([pscustomobject]@{Name='FixtureService';StartMode='Auto';State='Running'}) + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + $script:CurrentSnapshot=$path; $script:SuccessMessages=0 + Expect-Failure { Apply-StrictTelemetryServiceProfile } '*Injected denied service stop*' 'Strict privacy service failures propagate' + Check ($script:SuccessMessages -eq 0) 'Strict privacy does not print success after denied service mutation' + $snapshot.Services=@(); $snapshot.Tasks=@([pscustomobject]@{Path='\Fixture\';Name='FixtureTask';Enabled=$true}) + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + Expect-Failure { Apply-StrictTelemetryServiceProfile } '*Injected denied task disable*' 'Strict privacy task failures propagate' + Expect-Failure { Set-RegExact $key 'Fixture' 'bad' 'UnknownType' } '*Unsupported registry value type*' 'Unsupported registry kinds cannot silently become strings' + Set-RegExact $key 'Fixture' 'CaseOnly' String + function New-ItemProperty { } + Expect-Failure { Set-RegExact $key 'Fixture' 'unwritten' String } '*Registry write read-back failed*' 'Ignored registry writes cannot claim success' + Expect-Failure { Set-RegExact $key 'Fixture' 'caseonly' String } '*Registry write read-back failed*' 'Registry read-back preserves exact string case' + $psi=New-Object Diagnostics.ProcessStartInfo + $psi.FileName=Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + $psi.Arguments='-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "'+$privacy+'" -ExpectedUserSid S-1-5-21-0-0-0-99999' + $psi.UseShellExecute=$false; $psi.CreateNoWindow=$true; $psi.RedirectStandardOutput=$true; $psi.RedirectStandardError=$true + $foreignState=Join-Path $fixture 'ForeignProgramData' + $psi.EnvironmentVariables['ProgramData']=$foreignState + $psi.EnvironmentVariables['LOCALAPPDATA']=Join-Path $fixture 'ForeignLocalAppData' + $psi.EnvironmentVariables['WINDOWS_PC_TOOLKIT_DEBUG']='0' + $process=New-Object Diagnostics.Process; $process.StartInfo=$psi + try { + [void]$process.Start() + $stdout=$process.StandardOutput.ReadToEndAsync(); $stderr=$process.StandardError.ReadToEndAsync() + if (-not $process.WaitForExit(15000)) { $process.Kill(); throw 'Foreign-user entrypoint test did not exit.' } + $text=$stdout.Result+$stderr.Result + Check ($process.ExitCode -ne 0 -and $text -match 'elevated as a different Windows user' -and -not (Test-Path -LiteralPath $foreignState)) 'Native Privacy entrypoint rejects another user SID before state initialization' + } finally { $process.Dispose() } + } + + Import-Module (Join-Path $Root 'Modules\Network.State.psm1') -Force + $network=Get-Module Network.State + & $network { + $script:Writes=0 + function script:Get-DnsClientServerAddress { param($InterfaceIndex) [pscustomobject]@{AddressFamily=2} } + function script:Set-DnsClientServerAddress { param($InputObject,$ServerAddresses,[switch]$ResetServerAddresses) $script:Writes++ } + } + Check (& $network { (Get-Command Set-DnsClientServerAddress).CommandType -eq 'Function' }) 'Network writes are intercepted before testing' + Expect-Failure { Set-AdapterDnsFamilies -Index 7 -IPv4 @('9.9.9.9') -IPv6 @('2620:fe::fe') } '*does not expose address family 23*' 'Missing IPv6 family is rejected during preflight' + Check ((& $network { $script:Writes }) -eq 0) 'Missing IPv6 family cannot partially overwrite IPv4' + & $network { function script:Get-DnsClientServerAddress { @() } } + Expect-Failure { Set-AdapterDnsFamilies -Index 7 } '*exposes no DNS address families*' 'Missing adapter DNS inventory cannot report a successful reset' + + & { + $dns=Join-Path $Root 'dns-encrypted-doh\DNS_Manager.ps1' + if (-not (Test-Path -LiteralPath $dns)) { $dns=Join-Path $Root 'Pc Privacy Guard\Optional DNS\DNS_Manager.ps1' } + . $dns -Action Library + $DnsLog=Join-Path $fixture 'Logs\DNS.log'; $DnsDebug=$false + Write-DnsDiagnostic 'INFO' 'quiet-event' + Check (-not (Test-Path -LiteralPath $DnsLog)) 'DNS detailed events stay quiet by default' + Write-DnsDiagnostic 'FAIL' ('f'*5000) + $entry=Get-Content -LiteralPath $DnsLog -Raw -Encoding UTF8 | ConvertFrom-Json + Check ($entry.Version -eq $Version -and $entry.Kind -eq 'FAIL' -and $entry.Message.Length -eq 4096) 'DNS failures retain version and bounded cause while detailed logging is off' + $DnsDebug=$true; Write-DnsDiagnostic 'DEBUG' 'debug-event'; $DnsDebug=$false + Check (@(Get-Content -LiteralPath $DnsLog -Encoding UTF8).Count -eq 2) 'DNS debug logging can be toggled on' + Check (($Providers.Quad9.V6 -join ',') -eq '2620:fe::fe,2620:fe::9') 'Quad9 IPv6 addresses match the official Secure service table' + $StateRoot=Join-Path $fixture 'DNS'; $SnapshotRoot=Join-Path $StateRoot 'Snapshots' + $script:Entries=@{}; $script:Writes=0; $script:Rollback=0 + function Assert-Admin { } + function Write-Status { } + function Get-DnsClientDohServerAddress { @($script:Entries.Values) } + function Set-DnsClientDohServerAddress { param($ServerAddress,$DohTemplate,$AllowFallbackToUdp,$AutoUpgrade) $script:Writes++ } + function Add-DnsClientDohServerAddress { param($ServerAddress,$DohTemplate,$AllowFallbackToUdp,$AutoUpgrade) $script:Writes++ } + function Remove-DnsClientDohServerAddress { param($ServerAddress) $script:Writes++ } + function Set-AdapterDnsFamilies { param($Index,$IPv4,$IPv6) $script:Writes++ } + function Clear-DnsClientCache { } + $guid=[guid]::NewGuid() + function Get-Adapters { [pscustomobject]@{Name='Fixture NIC';ifIndex=7;InterfaceGuid=$guid} } + function Get-NetAdapter { Get-Adapters } + function Get-StaticDnsServers { param($Adapter) [pscustomobject]@{IPv4=@('9.9.9.9');IPv6=@()} } + function Get-DnsClientServerAddress { param($InterfaceIndex) [pscustomobject]@{AddressFamily=2;ServerAddresses=@('9.9.9.9')}; [pscustomobject]@{AddressFamily=23;ServerAddresses=@()} } + Check ((Get-Command Add-DnsClientDohServerAddress).CommandType -eq 'Function' -and (Get-Command Set-AdapterDnsFamilies).CommandType -eq 'Function') 'All DNS mutations are intercepted before testing' + Expect-Failure { Add-DohEntry '192.0.2.1' 'https://fixture.invalid/dns-query' } '*DoH write read-back failed*' 'Ignored DoH writes fail verification' + $script:Entries['192.0.2.1']=[pscustomobject]@{ServerAddress='192.0.2.1';DohTemplate='https://fixture.invalid/dns-query';AllowFallbackToUdp=$false;AutoUpgrade=$true} + Expect-Failure { Remove-DohEntry '192.0.2.1' } '*DoH removal read-back failed*' 'Ignored DoH removals fail verification' + $path=New-DnsSnapshot -TouchedServers @() + $snapshot=Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json + $snapshot.Adapters[0].StaticIPv4=@(); $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + $script:Writes=0 + Expect-Failure { Restore-Snapshot -Path $path } '*inconsistent address modes*' 'Malformed static DNS backup fails preflight' + Check ($script:Writes -eq 0) 'Malformed backup cannot modify encryption or adapters' + function Restore-Snapshot { param($Path) $script:Rollback++ } + Expect-Failure { Reset-Dhcp } '*Automatic DNS read-back failed*' 'DHCP reset cannot claim success when static DNS remains' + Check ($script:Rollback -eq 1) 'DHCP reset verification failure attempts prior-state rollback' + Set-Content -LiteralPath (Join-Path $StateRoot 'current_provider.json') -Value '{broken' -Encoding UTF8 + Expect-Failure { New-DnsSnapshot } '*' 'Unreadable current-provider state aborts the DNS snapshot' + } + Write-Host "DNS/privacy audit checks passed: $script:Checks" +} finally { + if (Test-Path -LiteralPath $key) { Remove-Item -LiteralPath $key -Recurse -Force } + if (Test-Path -LiteralPath $fixture) { Remove-Item -LiteralPath $fixture -Recurse -Force } + Remove-Module Network.State -ErrorAction SilentlyContinue +} diff --git a/tests/Test_AuditLaunchers.ps1 b/tests/Test_AuditLaunchers.ps1 new file mode 100644 index 0000000..18de9e2 --- /dev/null +++ b/tests/Test_AuditLaunchers.ps1 @@ -0,0 +1,72 @@ +#requires -version 5.1 +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +$label="Toolkit_LauncherAudit_' ! [1] "+[char]0x73A9+[char]0x5BB6+'_'+[guid]::NewGuid().ToString('N') +$fixture=Join-Path $env:TEMP $label +$checks=0 +function Check([bool]$Condition,[string]$Message) { if (-not $Condition) { throw "TEST FAILED: $Message" }; $script:checks++ } +$principal=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) +$admin=$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +$userSid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value +$launchers=@(Get-ChildItem -LiteralPath $root -Filter '*.bat' -Recurse -File) +Check ($launchers.Count -eq 12) 'All 12 batch launchers are covered' +[void][IO.Directory]::CreateDirectory($fixture) +[void][IO.Directory]::CreateDirectory((Join-Path $fixture 'Local\Temp')) +$markerScript=@' +param([string]$Action,[string]$ExpectedUserSid) +$ErrorActionPreference='Stop' +[IO.File]::WriteAllText($env:TOOLKIT_AUDIT_MARKER,([pscustomobject]@{Path=$PSCommandPath;Action=$Action;ExpectedUserSid=$ExpectedUserSid;Args=@($args)} | ConvertTo-Json -Compress)) +exit ([int]$env:TOOLKIT_AUDIT_EXIT) +'@ +try { + foreach ($launcher in $launchers) { + $relative=$launcher.FullName.Substring($root.TrimEnd('\').Length).TrimStart('\') + $destination=Join-Path $fixture $relative + [void][IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) + $content=[IO.File]::ReadAllText($launcher.FullName) + $target=[regex]::Match($content,'(?i)-File\s+"%~dp0([^"\r\n]+\.ps1)"') + Check $target.Success "$relative has an auditable PowerShell file target" + if ($target.Groups[1].Value -in @('PC_Cleaner.ps1','PC_Fixer.ps1','PC_Optimizer.ps1','Fix_VR_PCVR.ps1','PC_Privacy.ps1')) { + Check ($content -match '-ExpectedUserSid') "$relative requires the originating account identity for user-scoped actions" + } + if (-not $admin) { + # Fixture-only gate bypass; actual CMD/path/PowerShell target args stay intact. + $gate='(?m)^"%PS%" -NoProfile -Command "\$p=New-Object Security\.Principal\.WindowsPrincipal[^\r\n]*' + $matches=@([regex]::Matches($content,$gate)) + Check ($matches.Count -eq $(if ($relative -eq 'START_TOOLKIT.bat') { 0 } else { 1 })) "$relative uses the known principal gate; no UAC is allowed in a fixture" + $content=[regex]::Replace($content,$gate,'cmd /d /c exit 0') + } + [IO.File]::WriteAllText($destination,$content,[Text.Encoding]::ASCII) + $targetPath=Join-Path (Split-Path $destination -Parent) $target.Groups[1].Value + [IO.File]::WriteAllText($targetPath,$markerScript,(New-Object Text.UTF8Encoding($true))) + foreach ($expectedCode in @(0,7)) { + $marker=Join-Path $fixture ([guid]::NewGuid().ToString('N')+'.json') + $start=New-Object Diagnostics.ProcessStartInfo + $start.FileName=$env:ComSpec + $start.Arguments='/d /v:on /s /c ""'+$destination+'" "'+$userSid+'""' + $start.UseShellExecute=$false; $start.CreateNoWindow=$true + $start.RedirectStandardInput=$true; $start.RedirectStandardOutput=$true; $start.RedirectStandardError=$true + $start.EnvironmentVariables['TOOLKIT_AUDIT_MARKER']=$marker + $start.EnvironmentVariables['TOOLKIT_AUDIT_EXIT']=[string]$expectedCode + $start.EnvironmentVariables['LOCALAPPDATA']=(Join-Path $fixture 'Local') + $process=[Diagnostics.Process]::Start($start) + try { + $process.StandardInput.Close() + $stdout=$process.StandardOutput.ReadToEndAsync(); $stderr=$process.StandardError.ReadToEndAsync() + if (-not $process.WaitForExit(20000)) { $process.Kill(); throw "Timed out: $relative" } + $output=$stdout.Result+$stderr.Result + Check ($process.ExitCode -eq $expectedCode) "$relative propagates marker exit $expectedCode; actual $($process.ExitCode): $output" + Check (Test-Path -LiteralPath $marker) "$relative reaches only the harmless marker target" + $record=Get-Content -LiteralPath $marker -Raw -Encoding UTF8 | ConvertFrom-Json + Check ($record.Path -eq $targetPath) "$relative preserves the apostrophe/Unicode target path" + $action=[regex]::Match($content,'-Action\s+(\w+)') + if ($action.Success) { Check ($record.Action -eq $action.Groups[1].Value) "$relative preserves its action argument" } + if ($content -match '-ExpectedUserSid') { Check ($record.ExpectedUserSid -eq $userSid) "$relative forwards the originating Windows identity" } + } finally { $process.Dispose() } + } + } + Write-Host "PASS: $checks launcher checks across $($launchers.Count) real CMD entrypoints (administrator token: $admin)" + if (-not $admin) { Write-Host 'Fixture principal checks bypassed; interactive UAC was not exercised.' } +} finally { + if ([IO.Path]::GetFileName($fixture) -like 'Toolkit_LauncherAudit_*') { Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/tests/Test_AuditRepairGaming.ps1 b/tests/Test_AuditRepairGaming.ps1 new file mode 100644 index 0000000..9b45005 --- /dev/null +++ b/tests/Test_AuditRepairGaming.ps1 @@ -0,0 +1,234 @@ +#requires -version 5.1 +param([string]$Root=(Split-Path $PSScriptRoot -Parent)) +$ErrorActionPreference='Stop' +$checks=0 +function Check([bool]$Condition,[string]$Message) { if(-not $Condition){throw "TEST FAILED: $Message"}; $script:checks++ } +function Load-Functions([string]$Path,[string[]]$Names) { + $tokens=$null;$errors=$null + $ast=[Management.Automation.Language.Parser]::ParseFile($Path,[ref]$tokens,[ref]$errors) + if($errors.Count){throw ($errors.Message -join '; ')} + $text=@($ast.FindAll({param($n) $n -is [Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -in $Names},$false) | ForEach-Object {$_.Extent.Text}) -join "`n" + [scriptblock]::Create($text) +} +function Find-Source([string[]]$Candidates) { foreach($p in $Candidates){$path=Join-Path $Root $p;if(Test-Path -LiteralPath $path){return $path}};throw 'Component source missing.' } +$gaming=Find-Source @('pc-gaming-optimizer\Modules\Optimizer.Core.psm1','Pc Gaming Optimizer\Modules\Optimizer.Core.psm1') +$fixer=Find-Source @('pc-corruption-fixer\PC_Fixer.ps1','Pc Corruption Fixer\PC_Fixer.ps1') +$fixture=Join-Path $env:TEMP ("Toolkit audit O'Brien 玩家 "+[guid]::NewGuid().ToString('N')) +$key='HKCU:\Software\WindowsPCToolkitTests\'+[guid]::NewGuid().ToString('N') +[void][IO.Directory]::CreateDirectory($fixture) +try { + $module=Import-Module $gaming -Force -DisableNameChecking -PassThru + & $module { + param($fixture,$key) + $script:StateRoot=Join-Path $fixture 'GamingState' + $script:SnapshotRoot=Join-Path $script:StateRoot 'Snapshots' + $script:LogRoot=Join-Path $script:StateRoot 'Logs' + $script:DiagnosticPath=Join-Path $fixture 'Gaming.log' + $script:DebugLog=$false + $script:FixtureKey=$key + function script:Assert-Administrator {} + function script:Get-GamingRegistryTargets { @(@{Path=$script:FixtureKey;Name='Text'}) } + function script:Get-BcdOverrideStates { throw 'Unrequested BCD access' } + function script:Get-NetworkAdapterStates { throw 'Unrequested network access' } + function script:Read-Host { 'y' } + } $fixture $key + New-Item -Path $key -Force | Out-Null + foreach($item in @( + @{Name='Byte';Value=([byte[]]@(255));Kind='Binary'}, + @{Name='Multi';Value=([string[]]@('only'));Kind='MultiString'}, + @{Name='DWord';Value=-1;Kind='DWord'},@{Name='QWord';Value=([long]-1);Kind='QWord'}, + @{Name='Text';Value='CaseSensitive';Kind='String'})) { + Set-RegistryValueExact $key $item.Name $item.Value $item.Kind + $actual=Get-RegistryValueState $key $item.Name + Check ($actual.Kind -eq $item.Kind -and (ConvertTo-Json -InputObject $actual.Value -Compress) -ceq (ConvertTo-Json -InputObject $item.Value -Compress)) "Real registry round-trip: $($item.Kind)" + } + $path=New-GamingSnapshot -Reason 'isolated fixture' -RegistryTargets @(@{Path=$key;Name='Text'}) + $snapshot=Get-Content -LiteralPath $path -Raw | ConvertFrom-Json + Check ($snapshot.Schema -eq 3 -and $snapshot.Registry.Count -eq 1 -and $snapshot.Services.Count -eq 0 -and $snapshot.BcdOverrides.Count -eq 0 -and $snapshot.NetworkAdapters.Count -eq 0 -and $null -eq $snapshot.PowerScheme) 'New console snapshots capture only requested scope' + Check ($snapshot.UserSid -eq [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -and $snapshot.Computer -eq $env:COMPUTERNAME) 'Snapshots identify their originating computer/user' + Check ((Get-LatestGamingSnapshotPath) -ceq $path) 'UTF-8 snapshot pointer preserves Unicode/apostrophe path' + Set-RegistryValueExact $key 'Text' 'changed' String + Restore-LatestGamingSnapshot + Check ((Get-RegistryValueState $key 'Text').Value -ceq 'CaseSensitive') 'Confirmed console undo restores exact original case' + $snapshot.Computer='another-computer' + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + $blocked=$false;try{Restore-LatestGamingSnapshot}catch{$blocked=$true} + Check $blocked 'Foreign computer snapshot is rejected' + $snapshot.Computer=$env:COMPUTERNAME;$snapshot.Registry[0].Path='HKLM:\SOFTWARE\FixtureOutsideAllowlist' + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + $blocked=$false;try{Restore-LatestGamingSnapshot}catch{$blocked=$true} + Check $blocked 'Unrecognized registry target rejected before mutation' + $emptyPath=New-GamingSnapshot -Reason 'no registry changes' -RegistryTargets @() + $empty=Get-Content -LiteralPath $emptyPath -Raw | ConvertFrom-Json + Check ($empty.Registry.Count -eq 0 -and $empty.Services.Count -eq 0) 'Empty snapshots contain empty collections instead of null entries' + $empty.Services=@([pscustomobject]@{Name='SysMain';StartMode='Auto';State='Running'}) + $empty | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $emptyPath -Encoding UTF8 + & $module { function script:Set-Service { throw 'Injected service access denial' } } + $blocked=$false;try{Restore-LatestGamingSnapshot}catch{$blocked=$_.Exception.Message -like '*Injected service access denial*'} + Check $blocked 'Service restore errors propagate instead of claiming an exact successful restore' + + # Use real directories and a real junction; outside target must survive. + $oldLocal=$env:LOCALAPPDATA + try { + $env:LOCALAPPDATA=Join-Path $fixture 'Local' + $outside=Join-Path $fixture 'PersonalData' + [void][IO.Directory]::CreateDirectory($env:LOCALAPPDATA);[void][IO.Directory]::CreateDirectory($outside) + [IO.File]::WriteAllText((Join-Path $outside 'keep.bin'),'personal') + New-Item -ItemType Junction -Path (Join-Path $env:LOCALAPPDATA 'D3DSCache') -Target $outside | Out-Null + $ordinary=Join-Path $env:LOCALAPPDATA 'AMD\DxCache' + [void][IO.Directory]::CreateDirectory($ordinary);[IO.File]::WriteAllText((Join-Path $ordinary 'remove.bin'),'cache') + Invoke-ShaderCacheCleanup + Check (Test-Path -LiteralPath (Join-Path $outside 'keep.bin')) 'Shader cleanup preserves data behind a cache-root junction' + Check (-not (Test-Path -LiteralPath (Join-Path $ordinary 'remove.bin'))) 'Shader cleanup removes real ordinary cache files' + [IO.Directory]::Delete((Join-Path $env:LOCALAPPDATA 'D3DSCache')) + } finally { $env:LOCALAPPDATA=$oldLocal } + + & { + . (Load-Functions $gaming @('Invoke-SafeRetrim')) + $script:trimmed=@() + function Assert-Administrator {} + function Write-ToolStatus {} + function Get-PhysicalDisk { [pscustomobject]@{MediaType='SSD';SerialNumber='ssd1'};[pscustomobject]@{MediaType='HDD';SerialNumber='hdd1'} } + function Get-Disk { [pscustomobject]@{Number=1;SerialNumber='ssd1'};[pscustomobject]@{Number=2;SerialNumber='hdd1'};[pscustomobject]@{Number=3;SerialNumber='unknown'} } + function Get-Partition { param($DiskNumber) [pscustomobject]@{DiskNumber=$DiskNumber} } + function Get-Volume { process { [pscustomobject]@{DriveLetter=([string]$_.DiskNumber);DriveType='Fixed';FileSystem='NTFS'} } } + function Optimize-Volume { param($DriveLetter,[switch]$ReTrim,[switch]$Verbose,$ErrorAction) $script:trimmed+=@($DriveLetter) } + Invoke-SafeRetrim + Check ($script:trimmed.Count -eq 1 -and $script:trimmed[0] -eq '1') 'Console ReTrim excludes HDD/unknown media' + } + & { + . (Load-Functions $fixer @('Get-AiRegistryEntryState','Restore-AiFeatureSnapshot','Write-FixerDiagnostic')) + $script:Results=[ordered]@{};$script:HasFailure=$false;$script:Version='7.2.1' + $script:DebugLog=$false;$script:LogDir=Join-Path $fixture 'FixerLogs' + $script:AllowedKey=$key + function Get-AiFeaturePolicyDefinitions { foreach($name in @('Byte','Multi','DWord','QWord','Text')){[pscustomobject]@{Path=$script:AllowedKey;Name=$name}} } + function Write-Status {} + $entries=@(foreach($name in @('Byte','Multi','DWord','QWord','Text')){Get-AiRegistryEntryState $key $name}) + $snapshot=[pscustomobject]@{Schema=1;Computer=$env:COMPUTERNAME;User=[Security.Principal.WindowsIdentity]::GetCurrent().Name;Entries=$entries} + $path=Join-Path $fixture 'AI.json' + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + foreach($name in @('Byte','Multi','DWord','QWord','Text')) { New-ItemProperty -LiteralPath $key -Name $name -PropertyType String -Value 'changed' -Force | Out-Null } + Check (Restore-AiFeatureSnapshot $path) 'AI snapshot restores signed numeric and singleton array registry types' + foreach($entry in $entries) { + $actual=Get-AiRegistryEntryState $key $entry.Name + Check ($actual.Kind -eq $entry.Kind -and (ConvertTo-Json -InputObject $actual.Value -Compress) -ceq (ConvertTo-Json -InputObject $entry.Value -Compress)) "AI exact restoration: $($entry.Kind)" + } + $snapshot.Entries[0].Path='HKLM:\SOFTWARE\OutsideAllowedFixture' + $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8 + $blocked=$false;try{Restore-AiFeatureSnapshot $path}catch{$blocked=$true} + Check $blocked 'AI snapshot rejects unsupported registry destinations' + [void][IO.Directory]::CreateDirectory($script:LogDir) + Write-FixerDiagnostic Info 'quiet fixture' + Check (-not (Test-Path -LiteralPath (Join-Path $script:LogDir 'PCFixer.log'))) 'Fixer diagnostics are quiet by default' + Write-FixerDiagnostic Fail 'injected failure cause' + $diagnostic=Get-Content -LiteralPath (Join-Path $script:LogDir 'PCFixer.log') -Raw | ConvertFrom-Json + Check ($diagnostic.Version -eq '7.2.1' -and $diagnostic.Message -eq 'injected failure cause') 'Fixer error diagnostics record version and cause without debug enabled' + } + + # Harmless compiled argv fixture exercises the actual Win32 argument boundary. + $argvExe=Join-Path $fixture 'argv fixture.exe' + Add-Type -TypeDefinition @' +using System; +using System.Text; +public static class ToolkitAuditArgv { public static int Main(string[] args) { if (args.Length>0 && args[0]=="/enum") return 5; foreach (string a in args) Console.WriteLine("arg:"+Convert.ToBase64String(Encoding.UTF8.GetBytes(a))); return 0; } } +'@ -OutputAssembly $argvExe -OutputType ConsoleApplication + & { + . (Load-Functions $fixer @('Invoke-NativeProcessSafe')) + $script:ArgvExe=$argvExe + function Resolve-SystemTool { $script:ArgvExe } + $arguments=@('','two words','C:\Space Dir\','say"hello','four\\\"end','Grüße 玩家') + $result=Invoke-NativeProcessSafe 'fixture.exe' $arguments -Quiet + $actual=@($result.Output -split "`r?`n") + Check ($result.ExitCode -eq 0 -and $actual.Count -eq $arguments.Count) 'Native fallback launches real argv fixture and preserves empty arguments' + for($i=0;$i -lt $arguments.Count;$i++){Check ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($actual[$i].Substring(4))) -ceq $arguments[$i]) "Win32 quoting round-trip argument $i"} + } + & { + . (Load-Functions $gaming @('Get-BcdOverrideStates')) + $oldWindows=$env:SystemRoot + try { + $env:SystemRoot=Join-Path $fixture 'FakeWindows' + [void][IO.Directory]::CreateDirectory((Join-Path $env:SystemRoot 'System32')) + Copy-Item -LiteralPath $argvExe -Destination (Join-Path $env:SystemRoot 'System32\bcdedit.exe') + $blocked=$false;try{Get-BcdOverrideStates}catch{$blocked=$_.Exception.Message -like '*exit 5*'} + Check $blocked 'Real native BCD query failure cannot create an all-absent boot snapshot' + } finally { $env:SystemRoot=$oldWindows } + } + + & { + . (Load-Functions $fixer @('Show-Summary','Invoke-PerfCounterRepair','Invoke-QuickHealthScan','Invoke-StoreReset','Invoke-SfcScan','Invoke-DismRepair','Invoke-DeepCleanup','Invoke-TimeSyncRepair','Invoke-EventLogScan','Get-FixerEvents','Export-HealthReport','ConvertTo-HtmlSafe','Invoke-ServiceHealthCheck','Invoke-FullRepair')) + $script:Results=[ordered]@{};$script:HasFailure=$false;$script:RestartRequired=$false;$script:StartTime=Get-Date + $script:LogPath=Join-Path $fixture 'fixture.log';$script:Version='7.2.1';$script:Sys32=Join-Path $env:SystemRoot 'System32' + $B=@{TL='+';TR='+';BL='+';BR='+';H='-';V='|';LT='+';RT='+';MH='-'} + $script:Messages=New-Object Collections.ArrayList + function Write-Status { param($Type,$Text) [void]$script:Messages.Add("$Type $Text") } + function Write-StepHeader {} + function Get-Elapsed { 'fixture' } + function Enable-StayAwake {} + function Disable-StayAwake {} + function Start-Sleep {} + function Read-Host { 'n' } + function Test-PendingReboot { [pscustomobject]@{Pending=$false;Reasons=@()} } + $script:Results['SFC']='COMPLETED - Review SFC output' + Show-Summary + Check $true 'Summary accepts actual OrderedDictionary results without ContainsKey crash' + function Invoke-Native-Quiet { param($FilePath,$ArgumentList,$BaseDir) [pscustomobject]@{ExitCode=if($FilePath -eq 'winmgmt.exe'){5}else{0};Output='Native localized fixture verdict'} } + Invoke-PerfCounterRepair + Check ($script:Results['PerfCounters'] -like 'WARN*') 'WMI resync failure cannot produce a successful performance-counter report' + function Get-CimInstance { param($ClassName,$Filter) switch($ClassName){'Win32_LogicalDisk'{[pscustomobject]@{FreeSpace=80GB;Size=100GB;DeviceID='C:'}}} } + function Get-Service { param($Name) [pscustomobject]@{Name=$Name;DisplayName=$Name;Status=if($Name -eq 'wuauserv'){'Stopped'}else{'Running'};StartType=if($Name -eq 'wuauserv'){'Manual'}else{'Automatic'}} } + function Get-MpComputerStatus { [pscustomobject]@{AMServiceEnabled=$true;AntivirusSignatureLastUpdated=(Get-Date);QuickScanEndTime=(Get-Date)} } + function Get-PhysicalDisk { [pscustomobject]@{DeviceId='1';FriendlyName='Fixture';HealthStatus='Healthy'} } + Invoke-QuickHealthScan + Check ($script:Results['QuickScan'] -like 'COMPLETED*') 'Idle trigger-start Windows Update service is healthy; no false SFC integrity claim' + Check (-not @($script:Messages | Where-Object {$_ -match 'System file integrity OK|wuauserv.*NOT running|HEALTH SCAN COMPLETE - No issues found'}).Count) 'Quick scan avoids guessed native verdicts and false Windows Update failures' + $script:appxAllUsers=$false;$script:registered=0 + function Start-Process { [pscustomobject]@{ExitCode=0} } + function Get-AppxPackage { param([switch]$AllUsers) $script:appxAllUsers=[bool]$AllUsers;[pscustomobject]@{InstallLocation=$fixture} } + function Add-AppxPackage { $script:registered++ } + [IO.File]::WriteAllText((Join-Path $fixture 'AppxManifest.xml'),'') + Invoke-StoreReset + Check (-not $script:appxAllUsers -and $script:registered -eq 1) 'Store repair scopes registration to the current user' + $script:nativeCalled=$false + function Test-PendingReboot { [pscustomobject]@{Pending=$true;Reasons=@('Windows Update')} } + function Invoke-Native { $script:nativeCalled=$true;throw 'Servicing must not run' } + Invoke-SfcScan;Invoke-DeepCleanup + Check (-not $script:nativeCalled -and $script:Results['SFC'] -like 'SKIP*' -and $script:Results['DeepClean'] -like 'SKIP*') 'Pending servicing restart blocks SFC and deep cleanup' + function Test-PendingReboot { [pscustomobject]@{Pending=$false;Reasons=@()} } + $script:timeArguments=@() + function Start-ServiceSafely { $true } + function Invoke-Native-Quiet { param($FilePath,$ArgumentList) $script:timeArguments+=($ArgumentList -join ' ');[pscustomobject]@{ExitCode=5;Output='No time data available'} } + Invoke-TimeSyncRepair + Check (-not @($script:timeArguments | Where-Object {$_ -match '/unregister|/register'}).Count -and $script:Results['TimeSync'] -like 'FAIL*') 'Failed time resync preserves existing NTP/domain configuration' + function Get-WinEvent { [CmdletBinding()]param($FilterHashtable,$MaxEvents) Write-Error 'Access denied to fixture log' -ErrorId FixtureAccessDenied } + Invoke-EventLogScan + Check ($script:Results['EventLog'] -like 'WARN*') 'Unreadable event logs cannot become a clean-health verdict' + $script:LogDir=Join-Path $fixture 'Reports';[void][IO.Directory]::CreateDirectory($script:LogDir) + function Invoke-Item {} + Export-HealthReport + $html=Get-Content -LiteralPath (Get-ChildItem -LiteralPath $script:LogDir -Filter '*.html' | Select-Object -First 1).FullName -Raw + Check ($html -match 'Unavailable; event log could not be read' -and $html -notmatch '>0 critical/error events') 'HTML report shares event-log failure handling and cannot invent zero errors' + function Get-Service { param($Name) [pscustomobject]@{Name=$Name;DisplayName=$Name;Status='Stopped';StartType='Disabled'} } + Invoke-ServiceHealthCheck + Check ($script:Results['Services'] -like 'WARN*') 'Disabled critical services do not produce a successful service-health verdict' + } + foreach ($entry in @($fixer,(Find-Source @('pc-gaming-optimizer\PC_Optimizer.ps1','Pc Gaming Optimizer\PC_Optimizer.ps1')),(Find-Source @('pc-gaming-optimizer\Fix_VR_PCVR.ps1','Pc Gaming Optimizer\Fix_VR_PCVR.ps1')))) { + $tokens=$null;$errors=$null;$ast=[Management.Automation.Language.Parser]::ParseFile($entry,[ref]$tokens,[ref]$errors) + $guard=$ast.EndBlock.Statements | Where-Object { $_ -is [Management.Automation.Language.IfStatementAst] -and $_.Extent.Text -match 'ExpectedUserSid' } | Select-Object -First 1 + Check ($null -ne $guard -and $guard.Extent.StartOffset -lt ($ast.EndBlock.Statements | Where-Object { $_.Extent.Text -match '^Import-Module|^\$ProgressPreference|^\$ErrorActionPreference' } | Select-Object -First 1).Extent.StartOffset) 'Expected-user guard runs before console bootstrap' + $fixtureScript=Join-Path $fixture 'identity.ps1' + [IO.File]::WriteAllText($fixtureScript,"param([string]`$ExpectedUserSid)`r`n"+$guard.Extent.Text+"`r`nthrow 'Wrong-user guard did not stop execution'",(New-Object Text.UTF8Encoding($true))) + $previousPreference=$ErrorActionPreference + try { + $ErrorActionPreference='Continue' + $output=& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File $fixtureScript -ExpectedUserSid 'S-1-5-18' 2>&1 + $code=$LASTEXITCODE + } finally { $ErrorActionPreference=$previousPreference } + Check ($code -eq 1 -and ($output -join ' ') -notmatch 'Wrong-user guard did not stop execution') 'Actual user guard rejects another Windows account before bootstrap' + } + Write-Host "PASS: $checks repair/gaming audit checks. No live repair, optimization, service or network mutation was performed." -ForegroundColor Green +} finally { + Remove-Item -LiteralPath $key -Recurse -Force -ErrorAction SilentlyContinue + Remove-Module Optimizer.Core -Force -ErrorAction SilentlyContinue + if (-not [IO.Path]::GetFullPath($fixture).StartsWith([IO.Path]::GetFullPath($env:TEMP).TrimEnd('\')+'\',[StringComparison]::OrdinalIgnoreCase)) { throw 'Fixture cleanup path escaped TEMP.' } + if(Test-Path -LiteralPath $fixture){Remove-Item -LiteralPath $fixture -Recurse -Force} +} diff --git a/tests/Test_AuditSuite.ps1 b/tests/Test_AuditSuite.ps1 new file mode 100644 index 0000000..6697dd7 --- /dev/null +++ b/tests/Test_AuditSuite.ps1 @@ -0,0 +1,119 @@ +#requires -version 5.1 +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +$fixture=Join-Path $env:TEMP ('ToolkitAudit_'+[guid]::NewGuid().ToString('N')) +$key='HKCU:\Software\WindowsPCToolkitTests\'+[guid]::NewGuid().ToString('N') +$checks=0 +function Check([bool]$condition,[string]$message) { if (-not $condition) { throw "FAIL: $message" }; $script:checks++ } +function Fails([scriptblock]$body,[string]$message) { $failed=$false; try { & $body } catch { $failed=$true }; Check $failed $message } +[void][IO.Directory]::CreateDirectory($fixture) +try { + Import-Module (Join-Path $root 'Modules\Toolkit.Core.psm1') -ArgumentList (Join-Path $fixture 'State') -Force -DisableNameChecking + $module=Get-Module Toolkit.Core + & $module { function script:Assert-Administrator {} } + New-Item -Path $key -Force | Out-Null + New-ItemProperty -LiteralPath $key -Name Text -Value Original -PropertyType String | Out-Null + $run=New-SuiteRun 'CaseTest' + Set-SuiteRegistry $run $key Text original String + Check ($run.Registry.Count -eq 1 -and (Get-ItemPropertyValue $key Text) -ceq 'original') 'Case-only change is applied and journaled' + Set-ItemProperty -LiteralPath $key -Name Text -Value ORIGINAL + Fails { Undo-SuiteRegistryEntry $run.Registry[0] } 'Case-only intervening user change is preserved' + Check ((Get-ItemPropertyValue $key Text) -ceq 'ORIGINAL') 'User string casing survives undo conflict' + $run=New-SuiteRun 'Repair' + Fails { Invoke-SuiteUndo $run.Path } 'Repair/empty journals cannot claim successful undo' + $run.Path='C:\untrusted-report-path.json'; Save-SuiteJson (Join-Path (Get-SuiteDataRoot) ('Runs\'+$run.Id+'.json')) $run + $history=@(Get-SuiteHistory) + Check (@($history | Where-Object { $_.Path -eq 'C:\untrusted-report-path.json' }).Count -eq 0) 'History uses actual file path rather than a report-supplied path' + $run.Computer='OtherComputer'; Save-SuiteJson (Join-Path (Get-SuiteDataRoot) ('Runs\'+$run.Id+'.json')) $run + Check (@(Get-SuiteHistory | Where-Object Id -eq $run.Id).Count -eq 0) 'Foreign computer reports are excluded' + $preview=& $module { + try { + function script:Get-SuiteStartupItems { [pscustomobject]@{Id='startup';Name='Fixture Startup';Target='fixture.exe'} } + function script:Get-SuiteAppCatalog { [pscustomobject]@{Id='app';Name='Fixture App'} } + @(Get-SuitePreview -ActionIds @() -StartupIds startup -AppIds app) + } finally { Remove-Item Function:script:Get-SuiteStartupItems,Function:script:Get-SuiteAppCatalog } + } + Check ($preview.Count -eq 2 -and $preview[0].Category -eq 'Reviewed startup' -and $preview[1].Category -eq 'Reviewed apps') 'Preview covers reviewed-only selections' + $literalTask=& $module { + try { + function script:Get-ScheduledTask { @([pscustomobject]@{TaskName='[literal]';TaskPath='\Fixture\'},[pscustomobject]@{TaskName='l';TaskPath='\Fixture\'}) } + (Get-SuiteTask '\Fixture\' '[literal]').TaskName + } finally { Remove-Item Function:script:Get-ScheduledTask } + } + Check ($literalTask -eq '[literal]') 'Task selection rejects wildcard expansion to other tasks' + Check ((Get-Command Disable-ScheduledTask).Parameters.ContainsKey('InputObject') -and (Get-Command Enable-ScheduledTask).Parameters.ContainsKey('InputObject')) 'Real task mutation API supports exact task objects' + Set-ItemProperty -LiteralPath $key -Name Text -Value ('"'+$env:SystemRoot+'\System32\cmd.exe"') + $preserved=& $module { + param($key) + try { + $script:FixtureKey=$key + function script:Get-SuiteStartupItems { [pscustomobject]@{Kind='Startup';Missing=$true;Path=$script:FixtureKey;ValueName='Text'} } + function script:Get-RegistryIssues { } + $run=Invoke-SuiteRun -ActionIds RegistryCare -AppIds @() + $run.Registry.Count -eq 0 + } finally { Remove-Item Function:script:Get-SuiteStartupItems,Function:script:Get-RegistryIssues } + } $key + Check $preserved 'Shared registry care preserves a valid startup replacement after a stale missing-target scan' + $workerAst=[Management.Automation.Language.Parser]::ParseFile((Join-Path $root 'Toolkit.Worker.ps1'),[ref]$null,[ref]$null) + $switch=$workerAst.Find({param($n) $n -is [Management.Automation.Language.SwitchStatementAst]},$true) + $restore=[scriptblock]::Create(($switch.Clauses | Where-Object { $_.Item1.Value -eq 'RestorePoint' }).Item2.Extent.Text.TrimStart('{').TrimEnd('}')) + & { + param($body) + function Get-ComputerRestorePoint { [pscustomobject]@{SequenceNumber=1;Description='Windows PC Toolkit'} } + function Checkpoint-Computer { } + Fails { & $body } 'No-output restore-point command cannot claim a point was created' + $script:created=$false + function Checkpoint-Computer { $script:created=$true } + function Get-ComputerRestorePoint { [pscustomobject]@{SequenceNumber=$(if ($script:created) { 2 } else { 1 });Description='Windows PC Toolkit'} } + & $body + Check $script:created 'Restore-point success requires a new native sequence number' + } $restore + # Exercise the actual worker process for malformed requests and missing imports. + $request=Join-Path $fixture 'broken.json'; [IO.File]::WriteAllText($request,'invalid json') + Fails { Invoke-SuiteNative "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" @('-NoProfile','-File',(Join-Path $root 'Toolkit.Worker.ps1'),'-RequestPath',$request) } 'Malformed worker request exits unsuccessfully' + $result=Get-Content -LiteralPath ($request+'.result.json') -Raw | ConvertFrom-Json + Check ($result.Status -eq 'Failed' -and $result.Message) 'Malformed request produces visible failure result' + Copy-Item -LiteralPath (Join-Path $root 'Toolkit.Worker.ps1') -Destination $fixture + Fails { Invoke-SuiteNative "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" @('-NoProfile','-File',(Join-Path $fixture 'Toolkit.Worker.ps1'),'-RequestPath',$request) } 'Missing shared module exits unsuccessfully' + $result=Get-Content -LiteralPath ($request+'.result.json') -Raw | ConvertFrom-Json + Check ($result.Status -eq 'Failed' -and $result.Message -match 'Toolkit.Core') 'Missing module still produces failure result' + $outside=Join-Path $fixture 'outside'; [void][IO.Directory]::CreateDirectory($outside) + $link=Join-Path (Get-SuiteDataRoot) 'Requests' + [IO.Directory]::Delete($link) + & "$env:SystemRoot\System32\cmd.exe" /d /c "mklink /J `"$link`" `"$outside`"" | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Could not create owned junction fixture.' } + Check (-not (Test-SuiteMissingExecutable (Join-Path $link 'missing.exe'))) 'Linked executable locations cannot be classified as confirmed missing' + Fails { Initialize-SuiteState } 'Existing linked state folders are rejected before writes' + Check (@(Get-ChildItem -LiteralPath $outside).Count -eq 0) 'Linked destination remains untouched' + [IO.Directory]::Delete($link); $link=$null + $savedLocal=$env:LOCALAPPDATA; $savedDebug=$env:WINDOWS_PC_TOOLKIT_DEBUG + try { + $env:LOCALAPPDATA=Join-Path $fixture 'DiagnosticLocal'; $env:WINDOWS_PC_TOOLKIT_DEBUG=$null + Write-SuiteDiagnostic 'Quiet fixture' 'not logged' + $diagnostic=Join-Path $env:LOCALAPPDATA 'WindowsPCToolkit\Logs\Suite.log' + Check (-not (Test-Path -LiteralPath $diagnostic)) 'Default debug logging is quiet' + Write-SuiteDiagnostic 'Failure fixture' ('x'*6000) 'Error' + $line=Get-Content -LiteralPath $diagnostic -Raw | ConvertFrom-Json + Check ($line.Message.Length -eq 4096 -and $line.Version -eq '3.0.1') 'Failure causes are bounded and versioned with debug off' + $env:WINDOWS_PC_TOOLKIT_DEBUG='1' + Write-SuiteDiagnostic 'Startup fixture' 'DataRoot=fixture; Debug=True' + Check (@(Get-Content -LiteralPath $diagnostic).Count -eq 2) 'Explicit debug logging records state' + } finally { $env:LOCALAPPDATA=$savedLocal; $env:WINDOWS_PC_TOOLKIT_DEBUG=$savedDebug } + . (Join-Path $root 'Toolkit.ps1') -SmokeTest | Out-Null + foreach ($check in $script:optionChecks.Values) { $check.IsChecked=$false } + $script:controls.IncludeReviewed.IsChecked=$true + $script:controls.StartupGrid.ItemsSource=@([pscustomobject]@{Id='fixture';Name='Reviewed startup'}) + $script:controls.StartupGrid.SelectedIndex=0 + $script:captured=$null + function Start-ToolkitOperation { param($Operation,$Ids) $script:captured=$Operation } + Invoke-ToolkitButton $script:controls.OptimizeButton $null + Check ($script:captured -eq 'Optimize') 'Actual Optimize handler accepts reviewed-only startup selections' + Set-ToolkitBusy $true + Check (-not $script:controls.IncludeReviewed.IsEnabled -and -not $script:controls.RecommendedButton.IsEnabled -and -not $script:optionChecks.GameMode.IsEnabled) 'Running operation freezes profile/review controls' + $script:window.Close() + Write-Host "PASS: $checks Suite audit checks." +} finally { + if ($link -and (Test-Path -LiteralPath $link)) { [IO.Directory]::Delete($link) } + if ($key.StartsWith('HKCU:\Software\WindowsPCToolkitTests\') -and (Test-Path -LiteralPath $key)) { Remove-Item -LiteralPath $key -Recurse -Force } + if ([IO.Path]::GetFullPath($fixture).StartsWith([IO.Path]::GetFullPath($env:TEMP).TrimEnd('\')+'\',[StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $fixture -Recurse -Force } +} diff --git a/tests/Test_Toolkit.ps1 b/tests/Test_Toolkit.ps1 index c2c34a1..6eb2b47 100644 --- a/tests/Test_Toolkit.ps1 +++ b/tests/Test_Toolkit.ps1 @@ -166,6 +166,11 @@ try { $run=Invoke-SuiteRun -ActionIds @('Debloat') $copied=[IO.File]::ReadAllText((Join-Path $run.Apps[0].Backup 'payload.bin')) -eq 'package bytes' $removed=($run.Status -eq 'Completed' -and $run.Apps.Count -eq 1 -and -not $script:FixtureInstalled) + $backupPayload=Join-Path $run.Apps[0].Backup 'payload.bin' + [IO.File]::WriteAllText($backupPayload,'changed backup bytes') + $blockedUndo=Invoke-SuiteUndo $run.Path + $tamperBlocked=($blockedUndo.Status -eq 'UndoNeedsAttention' -and -not $script:FixtureInstalled) + [IO.File]::WriteAllText($backupPayload,'package bytes') $undo=Invoke-SuiteUndo $run.Path $reinstalled=($undo.Status -eq 'Restored' -and $script:FixtureInstalled) Remove-Item -LiteralPath (Join-Path $packageRoot 'AppxManifest.xml') -Force @@ -175,17 +180,23 @@ try { try { $null=Invoke-SuiteRun -ActionIds @('Debloat') -AppIds @('Microsoft.WindowsStore_fixture') } catch { $protectedBlocked=$true } function script:Get-CimInstance { [pscustomobject]@{BuildNumber='26100'} } function script:Get-WindowsOptionalFeature { param([switch]$Online,$FeatureName) [pscustomobject]@{FeatureName='Recall';State=$script:FixtureFeature} } - function script:Disable-WindowsOptionalFeature { param([switch]$Online,$FeatureName,[switch]$NoRestart) if (-not $NoRestart) { throw 'Restart must stay explicit' }; $script:FixtureFeatureCalls++; $script:FixtureFeature='Disabled'; [pscustomobject]@{RestartNeeded=$false} } + function script:Disable-WindowsOptionalFeature { param([switch]$Online,$FeatureName,[switch]$NoRestart) if (-not $NoRestart) { throw 'Restart must stay explicit' }; $script:FixtureFeatureCalls++; $script:FixtureFeature=if ($script:FixtureDisablePending) { 'DisablePending' } else { 'Disabled' }; [pscustomobject]@{RestartNeeded=$false} } function script:Enable-WindowsOptionalFeature { param([switch]$Online,$FeatureName,[switch]$NoRestart) if (-not $NoRestart) { throw 'Restart must stay explicit' }; $script:FixtureFeature='Enabled'; [pscustomobject]@{RestartNeeded=$false} } # Fake only reboot-key probes; use native filesystem access elsewhere. function script:Test-Path { param($LiteralPath) if ($LiteralPath -like 'HKLM:*') { return $false }; Microsoft.PowerShell.Management\Test-Path -LiteralPath $LiteralPath } + $script:FixtureDisablePending=$false $featureRun=Invoke-SuiteRun -ActionIds @('Recall') $featureDisabled=($featureRun.Status -eq 'Completed' -and $featureRun.Features.Count -eq 1 -and $script:FixtureFeature -eq 'Disabled') $featureUndo=Invoke-SuiteUndo $featureRun.Path $featureRestored=($featureUndo.Status -eq 'Restored' -and $script:FixtureFeature -eq 'Enabled') $script:FixtureFeature='Disabled' $alreadyDisabled=Invoke-SuiteRun -ActionIds @('Recall') - [pscustomobject]@{Allowlist=($available.Count -eq 1 -and $available[0].Name -eq 'Microsoft.Copilot');Preview=($preview[0].Details -match 'Installed: Microsoft.Copilot');Copied=$copied;Removed=$removed;Reinstalled=$reinstalled;BackupBlocked=$blocked;ProtectedBlocked=$protectedBlocked;FeatureDisabled=$featureDisabled;FeatureRestored=$featureRestored;FeatureSkipped=($alreadyDisabled.Features.Count -eq 0 -and $script:FixtureFeatureCalls -eq 1 -and $alreadyDisabled.Steps[0].Status -eq 'Skipped')} + $skipped=($alreadyDisabled.Features.Count -eq 0 -and $script:FixtureFeatureCalls -eq 1 -and $alreadyDisabled.Steps[0].Status -eq 'Skipped') + $script:FixtureFeature='Enabled'; $script:FixtureDisablePending=$true + $pending=Invoke-SuiteRun -ActionIds @('Recall') + $script:FixtureFeature='Disabled' # Simulate the requested restart completing. + $pendingUndo=Invoke-SuiteUndo $pending.Path + [pscustomobject]@{Allowlist=($available.Count -eq 1 -and $available[0].Name -eq 'Microsoft.Copilot');Preview=($preview[0].Details -match 'Installed: Microsoft.Copilot');Copied=$copied;Removed=$removed;TamperBlocked=$tamperBlocked;Reinstalled=$reinstalled;BackupBlocked=$blocked;ProtectedBlocked=$protectedBlocked;FeatureDisabled=$featureDisabled;FeatureRestored=$featureRestored;FeatureSkipped=$skipped;PendingRestored=($pendingUndo.Status -eq 'Restored' -and $script:FixtureFeature -eq 'Enabled')} } finally { Set-Item Function:script:Assert-Administrator $savedAdmin foreach ($name in @('Get-AppxPackage','Remove-AppxPackage','Add-AppxPackage','Get-CimInstance','Get-WindowsOptionalFeature','Disable-WindowsOptionalFeature','Enable-WindowsOptionalFeature','Test-Path')) { Remove-Item ('Function:script:'+$name) -ErrorAction SilentlyContinue } @@ -195,6 +206,8 @@ try { Check $debloat.Preview 'Debloat preview names the installed consumer packages' Check $debloat.Copied 'Real robocopy package backup preserves exact file bytes' Check $debloat.Removed 'One-click debloat backs up then invokes selected package removal' + Check $debloat.TamperBlocked 'Changed backup bytes block AppX registration and remain recoverable after correction' + Check $debloat.PendingRestored 'Recall undo accepts the expected disabled state after a pending restart finishes' Check $debloat.Reinstalled 'Package journal undo uses the saved manifest for re-registration' Check $debloat.BackupBlocked 'Incomplete package backup prevents removal and reports failure' Check $debloat.ProtectedBlocked 'Protected app IDs fail preflight before optimization' @@ -342,7 +355,7 @@ function Invoke-SuiteNative { $expected=if ($scenario -eq 'Fail') { 'CompletedWithErrors' } else { 'Completed' } Check ($result.Status -eq $expected -and $script:controls.RepairButton.IsEnabled) "Real repair button/worker/timer completes $scenario and re-enables controls" $calls=@([IO.File]::ReadAllLines($scenarioPath+'.calls')) - Check ($calls[0] -eq 'dism.exe' -and ($calls.Count -eq 1) -eq ($scenario -eq 'Fail') -and ($scenario -eq 'Fail' -or $calls[1] -eq 'sfc.exe')) "Repair runs DISM before SFC and stops on failure: $scenario" + Check ($calls[0] -eq 'dism.exe' -and ($calls.Count -eq 1) -eq ($scenario -ne 'Success') -and ($scenario -ne 'Success' -or $calls[1] -eq 'sfc.exe')) "Repair runs DISM before SFC and stops on failure or required restart: $scenario" Check ($script:controls.ResultText.Text -match $(if ($scenario -eq 'Fail') { 'repair-failure-marker' } else { 'repair-output-marker' }) -and $script:controls.ResultText.Text -match 'Report:') "Repair preserves actual native output and recovery report: $scenario" if ($scenario -eq 'Restart') { Check ($script:controls.ResultText.Text -match 'Restart required') 'Repair reports native restart-required exit code without restarting Windows' } } @@ -515,6 +528,7 @@ function Invoke-SuiteNative { $aiChecks=& { param($definitions,$key,$fixture) Invoke-Expression $definitions + function Get-AiFeaturePolicyDefinitions { [pscustomobject]@{Path=$key;Name='Expanded'} } $state=Get-AiRegistryEntryState $key 'Expanded' $snapshot=[pscustomobject]@{Schema=1;Computer=$env:COMPUTERNAME;User=[Security.Principal.WindowsIdentity]::GetCurrent().Name;Entries=@($state)} $path=Join-Path $fixture 'ai-policy.json'; Save-SuiteJson $path $snapshot From 99457f979bb1a18111e14469b5f0d783d191d186 Mon Sep 17 00:00:00 2001 From: Shugoki <35711149+ShugokiFable@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:53:36 -0300 Subject: [PATCH 2/2] Validate launcher paths through Windows aliases in both CI layouts --- .github/workflows/ci.yml | 2 +- tests/Test_AuditLaunchers.ps1 | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53e280d..7398fa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: foreach ($folder in $layout.Keys) { Copy-Item -LiteralPath (Join-Path $PWD $folder) -Destination (Join-Path $fixture $layout[$folder]) -Recurse } - foreach ($file in @('Toolkit.ps1','Toolkit.Worker.ps1','Toolkit.Run.ps1','Toolkit.xaml')) { + foreach ($file in @('Toolkit.ps1','Toolkit.Worker.ps1','Toolkit.Run.ps1','Toolkit.xaml','START_TOOLKIT.bat')) { Copy-Item -LiteralPath (Join-Path $PWD $file) -Destination $fixture } & "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -ExecutionPolicy Bypass -File (Join-Path $fixture 'tests\Test_All.ps1') diff --git a/tests/Test_AuditLaunchers.ps1 b/tests/Test_AuditLaunchers.ps1 index 18de9e2..fa0d740 100644 --- a/tests/Test_AuditLaunchers.ps1 +++ b/tests/Test_AuditLaunchers.ps1 @@ -58,7 +58,9 @@ try { Check ($process.ExitCode -eq $expectedCode) "$relative propagates marker exit $expectedCode; actual $($process.ExitCode): $output" Check (Test-Path -LiteralPath $marker) "$relative reaches only the harmless marker target" $record=Get-Content -LiteralPath $marker -Raw -Encoding UTF8 | ConvertFrom-Json - Check ($record.Path -eq $targetPath) "$relative preserves the apostrophe/Unicode target path" + $actualPath=[IO.Path]::GetFullPath($record.Path) + $expectedPath=[IO.Path]::GetFullPath($targetPath) + Check ($actualPath -eq $expectedPath) "$relative preserves the exact target file; actual=$actualPath; expected=$expectedPath; raw actual=$($record.Path); raw expected=$targetPath" $action=[regex]::Match($content,'-Action\s+(\w+)') if ($action.Success) { Check ($record.Action -eq $action.Groups[1].Value) "$relative preserves its action argument" } if ($content -match '-ExpectedUserSid') { Check ($record.ExpectedUserSid -eq $userSid) "$relative forwards the originating Windows identity" }