diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 0f7a5e5..92d28d5 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -21,11 +21,11 @@ jobs: dotnet-version: 10.0.x - name: Build executable shell: powershell - run: .\Build-Executable.ps1 -OutputDirectory .\dist\ci -Version 0.0.4 + run: .\Build-Executable.ps1 -OutputDirectory .\dist\ci -Version 0.0.5 - uses: actions/upload-artifact@v4 with: - name: ms-private-0.0.4 + name: ms-private-0.0.5 path: | - dist/ci/ms-private-0.0.4.exe - dist/ci/ms-private-0.0.4.exe.sha256 + dist/ci/ms-private-0.0.5.exe + dist/ci/ms-private-0.0.5.exe.sha256 dist/ci/SHA256SUMS.txt diff --git a/Accounts.ps1 b/Accounts.ps1 index ce93529..ed5f19c 100644 --- a/Accounts.ps1 +++ b/Accounts.ps1 @@ -7,6 +7,6 @@ if($Mode-in@('Audit','Plan')){$safe=@($plan|Select-Object Id,Kind,Label,Status,I if(-not$AcknowledgeAccountDataDeletion){throw 'Apply requires -AcknowledgeAccountDataDeletion. App data, tokens and matching stored credentials cannot be restored by ms-private.'} Assert-WindMutationAllowed if(-not$PSCmdlet.ShouldProcess('Current-user Microsoft application accounts and matching stored credentials','Permanently sign out and delete local account data')){return} -$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\accounts';[IO.Directory]::CreateDirectory($root)|Out-Null;$reportPath=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');$save={param($x)Save-MsPrivateAccountReport -Report $x -Path $reportPath}.GetNewClosure() +$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\accounts';[IO.Directory]::CreateDirectory($root)|Out-Null;$reportPath=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');$reportWriter=Get-Command Save-MsPrivateAccountReport -CommandType Function;$save={param($x)& $reportWriter -Report $x -Path $reportPath}.GetNewClosure() try{$report=Invoke-MsPrivateAccountPurge -Scope $Scope -Backend $backend -Save $save}catch{throw "Account purge stopped after a partial failure. Inspect the redacted report at $reportPath. $($_.Exception.Message)"} $report|Select-Object Kind,Scope,Status,CreatedUtc|Format-List;Write-Output "Account-purge report (no account names or secrets): $reportPath";Write-Output 'Restart Windows. Existing organizational registration and the Windows logon identity were not removed.' diff --git a/Build-AmePlaybook.ps1 b/Build-AmePlaybook.ps1 index ddf6840..4d40d57 100644 --- a/Build-AmePlaybook.ps1 +++ b/Build-AmePlaybook.ps1 @@ -1,5 +1,5 @@ #Requires -Version 5.1 -[CmdletBinding()]param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.4') +[CmdletBinding()]param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.5') Set-StrictMode -Version Latest;$ErrorActionPreference='Stop' if(-not$OutputDirectory){$OutputDirectory=Join-Path $PSScriptRoot 'dist'};$out=[IO.Path]::GetFullPath($OutputDirectory);[IO.Directory]::CreateDirectory($out)|Out-Null $seven=@((Get-Command 7z.exe -ErrorAction SilentlyContinue),(Get-Command (Join-Path $env:ProgramFiles '7-Zip\7z.exe') -ErrorAction SilentlyContinue))|Where-Object{$_}|Select-Object -First 1 @@ -15,5 +15,6 @@ try{ foreach($folder in @('src','config','docs')){Copy-Item -LiteralPath (Join-Path $PSScriptRoot $folder) -Destination $payload -Recurse} $target=Join-Path $out ("ms-private-$Version.apbx");if(Test-Path -LiteralPath $target){throw 'AME archive already exists. Use an empty output directory.'} Push-Location $stage;try{& $seven.Source a $target playbook.conf Executables Configuration '-pmalte'|Out-Host;if($LASTEXITCODE-ne 0){throw "7-Zip failed with exit code $LASTEXITCODE."}}finally{Pop-Location} - $hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant();[IO.File]::WriteAllText((Join-Path $out ("ms-private-$Version.apbx.sha256")),($hash+' '+[IO.Path]::GetFileName($target)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false));Write-Output "Created $target. SHA256: $hash" + $hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant();[IO.File]::WriteAllText((Join-Path $out ("ms-private-$Version.apbx.sha256")),($hash+' '+[IO.Path]::GetFileName($target)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false)) + $sumLines=@(Get-ChildItem -LiteralPath $out -File|Where-Object{$_.Extension-in@('.exe','.zip','.apbx')}|Sort-Object Name|ForEach-Object{((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant())+' '+$_.Name});[IO.File]::WriteAllText((Join-Path $out 'SHA256SUMS.txt'),(($sumLines-join[Environment]::NewLine)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false));Write-Output "Created $target. SHA256: $hash" }finally{if(Test-Path -LiteralPath $stage){Remove-Item -LiteralPath $stage -Recurse -Force}} diff --git a/Build-Executable.ps1 b/Build-Executable.ps1 index 51f070d..ca24a68 100644 --- a/Build-Executable.ps1 +++ b/Build-Executable.ps1 @@ -1,5 +1,5 @@ #Requires -Version 5.1 -[CmdletBinding()]param([string]$OutputDirectory,[string]$Version='0.0.4') +[CmdletBinding()]param([string]$OutputDirectory,[string]$Version='0.0.5') Set-StrictMode -Version Latest;$ErrorActionPreference='Stop' if($Version-notmatch'^\d+\.\d+\.\d+$'){throw 'Version must use major.minor.patch.'} $root=[IO.Path]::GetFullPath($PSScriptRoot);if(-not$OutputDirectory){$OutputDirectory=Join-Path $root ('dist\ms-private-'+$Version+'-final-'+(Get-Date -Format yyyyMMdd))};$output=[IO.Path]::GetFullPath($OutputDirectory);[IO.Directory]::CreateDirectory($output)|Out-Null diff --git a/Build-Release.ps1 b/Build-Release.ps1 index aade638..7f125c6 100644 --- a/Build-Release.ps1 +++ b/Build-Release.ps1 @@ -1,6 +1,6 @@ #Requires -Version 5.1 [CmdletBinding()] -param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.4') +param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.5') Set-StrictMode -Version Latest $ErrorActionPreference='Stop' if (-not $OutputDirectory) { $OutputDirectory=Join-Path $PSScriptRoot 'dist' } @@ -25,5 +25,6 @@ try { } } finally {$zip.Dispose()} $hash=(Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant() -[IO.File]::WriteAllText((Join-Path $output 'SHA256SUMS.txt'),($hash+' '+[IO.Path]::GetFileName($zipPath)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false)) +$sumLines=@(Get-ChildItem -LiteralPath $output -File|Where-Object{$_.Extension-in@('.exe','.zip','.apbx')}|Sort-Object Name|ForEach-Object{((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant())+' '+$_.Name}) +[IO.File]::WriteAllText((Join-Path $output 'SHA256SUMS.txt'),(($sumLines-join[Environment]::NewLine)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false)) Write-Output "Created $zipPath ($($files.Count) source files). SHA256: $hash" diff --git a/Hardware.ps1 b/Hardware.ps1 index 90e892e..ac44feb 100644 --- a/Hardware.ps1 +++ b/Hardware.ps1 @@ -5,4 +5,4 @@ if($Mode -eq 'List'){$x=@(Get-WindSensorInventory);if($Json){$x|ConvertTo-Json - if($Mode -eq 'Plan'){$x=@(Get-WindSensorPlan -InstanceId $InstanceId);if($Json){$x|ConvertTo-Json -Depth 5}else{$x|Format-Table -Wrap};return} Assert-WindMutationAllowed;$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\hardware';[IO.Directory]::CreateDirectory($root)|Out-Null if($Mode-eq'Apply'){if(-not$InstanceId.Count){throw 'Apply requires at least one exact -InstanceId.'};if(-not$Journal){$Journal=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')};Invoke-WindSensorApply -InstanceId $InstanceId -JournalPath $Journal -Confirm:$false;Write-Output "Hardware journal: $Journal";return} -if(-not$Journal){throw 'Restore requires -Journal.'};$j=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$save={param($x)Save-WindJournal -Journal $x -Path $Journal}.GetNewClosure();Restore-WindSensors -Journal $j -Save $save -Confirm:$false +if(-not$Journal){throw 'Restore requires -Journal.'};$j=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$journalWriter=Get-Command Save-WindJournal -CommandType Function;$save={param($x)& $journalWriter -Journal $x -Path $Journal}.GetNewClosure();Restore-WindSensors -Journal $j -Save $save -Confirm:$false diff --git a/Network.ps1 b/Network.ps1 index 8095c8a..ec198ca 100644 --- a/Network.ps1 +++ b/Network.ps1 @@ -40,5 +40,5 @@ if($Mode -eq 'Apply'){ } if(-not $Journal){throw "$Mode requires -Journal."} $saved=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json -$save={param($x) Save-WindJournal -Journal $x -Path $Journal}.GetNewClosure() +$journalWriter=Get-Command Save-WindJournal -CommandType Function;$save={param($x)& $journalWriter -Journal $x -Path $Journal}.GetNewClosure() if($Mode-eq'Confirm'){Confirm-WindNetwork -Journal $saved -Save $save|Format-List}else{Restore-WindNetwork -Journal $saved -Save $save|Format-List} diff --git a/README.md b/README.md index 10735d0..ccedaeb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ## Run -Download `ms-private-0.0.4.exe`, verify the adjacent SHA-256 file, then double-click the executable. Windows shows a UAC elevation prompt because the executable contains a `requireAdministrator` application manifest. Run it from the Windows account whose current-user apps and settings you intend to change; entering credentials for another administrator can target that account instead. +Download `ms-private-0.0.5.exe`, verify the adjacent SHA-256 file, then double-click the executable. Windows shows a UAC elevation prompt because the executable contains a `requireAdministrator` application manifest. Run it from the Windows account whose current-user apps and settings you intend to change; entering credentials for another administrator can target that account instead. The executable contains the reviewed runtime and does not fetch code. It validates the embedded manifest and every SHA-256 before extracting to `%ProgramFiles%\ms-private\runtime\-`, validates the installed copy on every launch, repairs modified or extra runtime files, removes older runtime versions, then opens the graphical wizard with Windows PowerShell 5.1. The executable is currently unsigned, so Windows may show an unknown-publisher warning. @@ -185,6 +185,6 @@ Read the [complete tool breakdown](docs/TOOL-BREAKDOWN.md), [current roadmap](do .\Build-Release.ps1 ``` -Tests use synthetic registry/app/network/service/account backends, read-only security inventory and temporary workspace files. CI runs 117 PowerShell checks on Windows PowerShell 5.1 and PowerShell 7, plus ten native launcher checks, PE inspection and the one-file publish. The source is ready for review as a **release candidate**; full live uninstall/sign-out/reboot/update/network/device validation on the supported OS matrix remains required before advertising a production-validated release. See [validation](docs/VALIDATION.md) and [contributing](CONTRIBUTING.md). +Tests use synthetic registry/app/network/service/account backends, read-only security inventory and temporary workspace files. CI runs 120 PowerShell checks on Windows PowerShell 5.1 and PowerShell 7, plus ten native launcher checks, PE inspection and the one-file publish. The source is ready for review as a **release candidate**; full live uninstall/sign-out/reboot/update/network/device validation on the supported OS matrix remains required before advertising a production-validated release. See [validation](docs/VALIDATION.md) and [contributing](CONTRIBUTING.md). `Build-Executable.ps1` creates the self-contained x64 executable and checksum. `Build-Release.ps1` writes a source ZIP and checksum, and `Build-AmePlaybook.ps1` creates the separate AME wrapper when 7-Zip is installed. Builders exclude local reports, refuse to overwrite an existing artifact and do not sign or publish it. diff --git a/Recall.ps1 b/Recall.ps1 index e8bc9c2..5edbdc8 100644 --- a/Recall.ps1 +++ b/Recall.ps1 @@ -5,4 +5,4 @@ Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join if($Mode -in @('Audit','Plan')){$x=Get-WindRecallPlan -RemoveFeature:$RemoveFeature;if($Json){$x|ConvertTo-Json -Depth 8}else{$x.Items|Format-Table -Wrap};return} Assert-WindMutationAllowed;$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\recall';[IO.Directory]::CreateDirectory($root)|Out-Null if($Mode -eq 'Apply'){if(-not $Journal){$Journal=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')};Invoke-WindRecallApply -RemoveFeature:$RemoveFeature -AcknowledgeSnapshotDeletion:$AcknowledgeSnapshotDeletion -JournalPath $Journal -Confirm:$false;Write-Output "Recall journal: $Journal";return} -if(-not $Journal){throw 'Restore requires -Journal.'};$j=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$save={param($x)Save-WindJournal -Journal $x -Path $Journal}.GetNewClosure();Restore-WindRecall -Journal $j -Save $save -Confirm:$false +if(-not $Journal){throw 'Restore requires -Journal.'};$j=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$journalWriter=Get-Command Save-WindJournal -CommandType Function;$save={param($x)& $journalWriter -Journal $x -Path $Journal}.GetNewClosure();Restore-WindRecall -Journal $j -Save $save -Confirm:$false diff --git a/Services.ps1 b/Services.ps1 index b935364..c07e005 100644 --- a/Services.ps1 +++ b/Services.ps1 @@ -7,5 +7,6 @@ Assert-WindMutationAllowed $root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\services';[IO.Directory]::CreateDirectory($root)|Out-Null if($Mode-eq'Apply'){$plan=@(Get-MsPrivateServicePlan -Profile $Profile -Backend $backend);$Journal=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')}elseif([string]::IsNullOrWhiteSpace($Journal)){throw 'Restore requires -Journal.'} if(-not$PSCmdlet.ShouldProcess('Selected Windows privacy services',$Mode)){return} -if($Mode-eq'Apply'){$journal=$null;$save={param($x)Save-MsPrivateServiceJournal -Journal $x -Path $Journal}.GetNewClosure();$journal=Invoke-MsPrivateServiceApply -Plan $plan -Backend $backend -Save $save}else{$journal=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$save={param($x)Save-MsPrivateServiceJournal -Journal $x -Path $Journal}.GetNewClosure();$journal=Restore-MsPrivateServices -Journal $journal -Backend $backend -Save $save} +$journalWriter=Get-Command Save-MsPrivateServiceJournal -CommandType Function;$save={param($x)& $journalWriter -Journal $x -Path $Journal}.GetNewClosure() +if($Mode-eq'Apply'){$journal=$null;$journal=Invoke-MsPrivateServiceApply -Plan $plan -Backend $backend -Save $save}else{$journal=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$journal=Restore-MsPrivateServices -Journal $journal -Backend $backend -Save $save} $journal|Select-Object Kind,Status,CreatedUtc|Format-List;Write-Output "Service recovery journal: $Journal" diff --git a/ame/src/playbook.conf b/ame/src/playbook.conf index e439b82..04b9109 100644 --- a/ame/src/playbook.conf +++ b/ame/src/playbook.conf @@ -6,7 +6,7 @@ ms-private Launches the ms-private graphical wizard. ms-private preserves Defender, Windows Firewall, Windows Update, security logs, WebView2, drivers and runtimes.
Every selected control is previewed and applied by the same PowerShell engine used by the standalone package.
- 0.0.4 + 0.0.5 64d76b08-f58d-499c-aadc-1d72d620e704 false diff --git a/docs/PLAN.md b/docs/PLAN.md index 914a4be..6fa5a70 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -4,7 +4,7 @@ Build a Windows privacy wizard with three browser choices: Tor Browser, Zen and Helium. Its job is to reduce collection and retention, expose tradeoffs, and verify specific settings. It must never present a percentage-anonymous score or claim that a host is untraceable. -The current release-candidate version is 0.0.4. See [ROADMAP.md](ROADMAP.md) for the reconciled open and validation work and [NATIVE-API.md](NATIVE-API.md) for direct API and security-product behavior. +The current release-candidate version is 0.0.5. See [ROADMAP.md](ROADMAP.md) for the reconciled open and validation work and [NATIVE-API.md](NATIVE-API.md) for direct API and security-product behavior. The running Windows installation is the development host. Development and read-only inspection are authorized; this iteration creates the tool and planning artifacts without destructive OS changes or installation of applications on that host. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index def5974..0f3926a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ -# ms-private 0.0.4 roadmap +# ms-private 0.0.5 roadmap -Version 0.0.4 is a published prerelease. Implementation exists for the graphical and terminal interfaces, presets, policy engine, debloat, account purge, service control, network modes, Recall, sensors, cleanup, lock handling, security audit, source/AME packaging and the self-contained executable. The remaining work is mostly live acceptance, signing provenance and the separately designed offline-maintenance system. +Version 0.0.5 is a prerelease. Implementation exists for the graphical and terminal interfaces, presets, policy engine, debloat, account purge, service control, network modes, Recall, sensors, cleanup, lock handling, security audit, source/AME packaging and the self-contained executable. The remaining work is mostly live acceptance, signing provenance and the separately designed offline-maintenance system. ## Open work diff --git a/docs/STATUS.md b/docs/STATUS.md index 934fcd2..1586df5 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -23,11 +23,11 @@ The supplied script was parsed without execution. Three isolated in-memory probe - Exact selected PnP sensor disable/restore; allowlisted Temp, crash-report, Recent/Jump List, thumbnail, PowerShell-history and expired-report cleanup; scheduling; and SessionLock clipboard/VeraCrypt handling. - Read-only BitLocker/device-encryption, Defender/cloud posture, firewall, Secure Boot, TPM, VBS/Credential Guard, LSA, UAC, SMB/SMB1, remote-access and protected-service status without recovery keys, account names, Defender exclusions or secrets. - AME wrapper source and builder that launch the same graphical wizard without disabling Defender or Windows Update. -- Self-contained x64 `ms-private-0.0.4.exe` launcher with a `requireAdministrator` manifest, embedded hash-manifested runtime, exact-file verification, tamper repair and protected Program Files installation. +- Self-contained x64 `ms-private-0.0.5.exe` launcher with a `requireAdministrator` manifest, embedded hash-manifested runtime, exact-file verification, tamper repair and protected Program Files installation. ## Verification performed -- 117 tests pass in PowerShell 7 and Windows PowerShell 5.1: 25 policy/transaction, 29 app/menu, 19 offline-bundle and 44 feature/package/security/service/account/preset/operation-runner checks. The runner writes a real report path during regression testing, and the GUI child-script invoker is exercised across the same nested module boundary used by Apply. +- 120 tests pass in PowerShell 7 and Windows PowerShell 5.1: 25 policy/transaction, 29 app/menu, 19 offline-bundle and 47 feature/package/security/service/account/preset/operation-runner checks. The runner writes real populated and empty reports during regression testing. GUI child-script invocation and deferred atomic writers are exercised across the same nested module boundaries used by Apply. - Ten native launcher tests pass for archive and installed-manifest integrity, installation validation, modified/extra-file repair, path traversal, bad hashes and undeclared entries. Managed PE inspection verifies AMD64 GUI format, the UAC manifest, version, checksum and signature state. - Shared journal/report create, flush, atomic-replace and parse tests passed using temporary workspace files. - All PowerShell source files parsed successfully during verification. diff --git a/docs/TOOL-BREAKDOWN.md b/docs/TOOL-BREAKDOWN.md index 92158f9..6954105 100644 --- a/docs/TOOL-BREAKDOWN.md +++ b/docs/TOOL-BREAKDOWN.md @@ -6,7 +6,7 @@ ms-private is a local Windows privacy and debloat toolkit. It builds an exact pl | Interface | Entry point | Use | | --- | --- | --- | -| Single-file Windows release | `ms-private-0.0.4.exe` | Recommended launch path. Requests UAC, verifies and installs the embedded runtime, then opens the graphical wizard. | +| Single-file Windows release | `ms-private-0.0.5.exe` | Recommended launch path. Requests UAC, verifies and installs the embedded runtime, then opens the graphical wizard. | | Graphical wizard | `ms-private-GUI.ps1` | Tabbed Windows Forms interface with one exact-plan review and final approval. | | Terminal wizard | `ms-private.ps1` | Arrow, Space, Enter and Esc navigation with searchable app selection. | | Complete preset CLI | `Preset.ps1` | Auto-detect or plan/apply EstablishedSafe, FreshInstall, Gentle, Medium, Strong or Maximum without navigating the UI. | @@ -107,7 +107,7 @@ Reports are local and have no automatic upload. Installed-app inventories and ne ## Running a reviewed download -For a normal release, verify `ms-private-0.0.4.exe.sha256` and launch `ms-private-0.0.4.exe`. The executable is self-contained for Windows x64 and requests administrator rights through its application manifest. It embeds the source runtime, verifies the archive manifest and every file hash, extracts beneath `%ProgramFiles%\ms-private\runtime`, revalidates that copy at every launch, and repairs modifications or undeclared files. It is not code-signed yet, so the publisher warning is expected until a signing and provenance pipeline is added. +For a normal release, verify `ms-private-0.0.5.exe.sha256` and launch `ms-private-0.0.5.exe`. The executable is self-contained for Windows x64 and requests administrator rights through its application manifest. It embeds the source runtime, verifies the archive manifest and every file hash, extracts beneath `%ProgramFiles%\ms-private\runtime`, revalidates that copy at every launch, and repairs modifications or undeclared files. It is not code-signed yet, so the publisher warning is expected until a signing and provenance pipeline is added. The PowerShell implementation remains visible in the source release and inside the hash-manifested payload. Moving the policy, recovery, Appx, WinGet and firewall logic into another language would not itself improve privacy; the native launcher supplies one-file delivery, elevation and runtime integrity while retaining the tested engine. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index fcba583..e05df2f 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -2,7 +2,7 @@ ## Completed checks -Release-candidate update, 2026-09-24: 117 tests pass in each of Windows PowerShell 5.1 and PowerShell 7 (25 policy/transaction, 29 app/menu, 19 offline-bundle, 44 feature/package/security/service/account/preset/operation-runner). The policy tests include current-user registry scope, Home direct-control selection and managed-edition exclusion, and Enterprise-only selection. The app tests include aggressive all-removable-app and explicit Store selection, safe skipping of ambiguous desktop registrations, protection of security/dependency applications, completeness of FreshInstall/Medium/Strong/Maximum and the non-destructive EstablishedSafe preset, the reported JSON catalog failure and journal callback scope failure, browser-before-removal sequencing, installer errors, stale inventory, partial failure, bundle deduplication, group selection, keyboard navigation, filtering and terminal layout limits. Feature tests cover real operation-runner report creation/finalization, captured GUI child-script invocation across the nested execution-module boundary, invoker path-traversal refusal, fresh/established/unknown install-age decisions, continued execution of independent groups after a failure, dependency skipping, preset schema refusal, shared Recall apply/skip/conflict decisions, atomic JSON replacement, malformed or duplicate policy/service/account/network catalogs, protected update-endpoint refusal, untrusted registry-hive refusal, network journal recovery, cleanup-root and protected-log refusal, recovery-journal exclusion, browser-cache scope, protected-service exclusion, service disable/restore, forged service-plan rejection, exact account-package reset, credential allowlisting, report redaction, VeraCrypt volume validation, versioned secret-free security audit behavior and AME manifest structure. Native arrow/Space/Enter/Esc browser selection was also exercised in a real Windows PowerShell console. Live host inventory, service planning and account planning were read-only. No real uninstall, account purge, policy or service write, reboot, firewall change, capture, device disable, Recall deletion or update installation was performed. +Release-candidate update, 2026-09-24: 120 tests pass in each of Windows PowerShell 5.1 and PowerShell 7 (25 policy/transaction, 29 app/menu, 19 offline-bundle, 47 feature/package/security/service/account/preset/operation-runner). The policy tests include current-user registry scope, Home direct-control selection and managed-edition exclusion, and Enterprise-only selection. The app tests include aggressive all-removable-app and explicit Store selection, safe skipping of ambiguous desktop registrations, protection of security/dependency applications, completeness of FreshInstall/Medium/Strong/Maximum and the non-destructive EstablishedSafe preset, the reported JSON catalog failure and journal callback scope failure, browser-before-removal sequencing, installer errors, stale inventory, partial failure, bundle deduplication, group selection, keyboard navigation, filtering and terminal layout limits. Feature tests cover real populated and empty operation-runner reports, captured GUI child-script invocation and deferred atomic JSON writers across nested module boundaries, cleanup report finalization, invoker path-traversal refusal, fresh/established/unknown install-age decisions, continued execution of independent groups after a failure, dependency skipping, preset schema refusal, shared Recall apply/skip/conflict decisions, atomic JSON replacement, malformed or duplicate policy/service/account/network catalogs, protected update-endpoint refusal, untrusted registry-hive refusal, network journal recovery, cleanup-root and protected-log refusal, recovery-journal exclusion, browser-cache scope, protected-service exclusion, service disable/restore, forged service-plan rejection, exact account-package reset, credential allowlisting, report redaction, VeraCrypt volume validation, versioned secret-free security audit behavior and AME manifest structure. Native arrow/Space/Enter/Esc browser selection was also exercised in a real Windows PowerShell console. Live host inventory, service planning and account planning were read-only. No real uninstall, account purge, policy or service write, reboot, firewall change, capture, device disable, Recall deletion or update installation was performed. The .NET launcher has ten passing tests for valid archive acceptance, installed payload and manifest verification, repair after modified files, rejection and repair of undeclared installed files, manifest path traversal refusal, bad-hash refusal and undeclared archive-entry refusal. `Build-Executable.ps1` must also produce exactly one x64 self-contained executable plus its checksum. Its managed PE inspection verifies AMD64 and GUI headers, the embedded `requireAdministrator` manifest, version, checksum and signature state. Before publishing, launch the GUI in a clean VM, alter and add runtime files to confirm repair, and test a Program Files access failure. diff --git a/native/MsPrivate.Launcher/MsPrivate.Launcher.csproj b/native/MsPrivate.Launcher/MsPrivate.Launcher.csproj index 7ac4db2..65eeb50 100644 --- a/native/MsPrivate.Launcher/MsPrivate.Launcher.csproj +++ b/native/MsPrivate.Launcher/MsPrivate.Launcher.csproj @@ -19,9 +19,9 @@ app.manifest ms-private MsPrivate.Launcher - 0.0.4 - 0.0.4.0 - 0.0.4.0 + 0.0.5 + 0.0.5.0 + 0.0.5.0 ms-private Windows privacy control center diff --git a/src/MsPrivate.Execution.psm1 b/src/MsPrivate.Execution.psm1 index afff1d8..2d5b37d 100644 --- a/src/MsPrivate.Execution.psm1 +++ b/src/MsPrivate.Execution.psm1 @@ -17,7 +17,7 @@ function New-MsPrivateScriptInvoker { function Invoke-MsPrivateGroupPlan { param( - [Parameter(Mandatory)][object[]]$Groups, + [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Groups, [scriptblock]$OnProgress, [string]$ReportPath ) diff --git a/src/MsPrivate.IO.psm1 b/src/MsPrivate.IO.psm1 index 2f4ebad..0fd27ce 100644 --- a/src/MsPrivate.IO.psm1 +++ b/src/MsPrivate.IO.psm1 @@ -38,4 +38,15 @@ function Write-MsPrivateJsonAtomic { } } -Export-ModuleMember -Function Write-MsPrivateJsonAtomic +function New-MsPrivateJsonWriter { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Path, + [ValidateRange(2,100)][int]$Depth=12 + ) + $target=[IO.Path]::GetFullPath($Path) + $writer=Get-Command Write-MsPrivateJsonAtomic -CommandType Function + {param($value)& $writer -InputObject $value -Path $target -Depth $Depth}.GetNewClosure() +} + +Export-ModuleMember -Function Write-MsPrivateJsonAtomic,New-MsPrivateJsonWriter diff --git a/src/Wind.Cleanup.psm1 b/src/Wind.Cleanup.psm1 index fbdd268..84c8e3c 100644 --- a/src/Wind.Cleanup.psm1 +++ b/src/Wind.Cleanup.psm1 @@ -88,7 +88,7 @@ function Invoke-WindCleanup { if($Plan.Schema-ne 2-or$Plan.AgeDays-lt 0){throw 'Invalid cleanup plan.'};$valid=@('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports');if(@($Plan.Categories|Where-Object{$_-notin$valid}).Count){throw 'Cleanup plan contains an unknown category.'} $catalog=@(Get-WindPrivacyCleanupCatalog|Where-Object{$Plan.Categories-contains$_.Category});$cutoff=[datetime]::Parse($Plan.CutoffUtc).ToUniversalTime() $report=[pscustomobject]@{Schema=2;Kind='WindCleanup';CreatedUtc=[DateTime]::UtcNow.ToString('o');Categories=@($Plan.Categories);Status='Running';Deleted=0;Skipped=0;FreedBytes=[long]0;Entries=@()} - $save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $ReportPath -Depth 7}.GetNewClosure();&$save $report + $save=New-MsPrivateJsonWriter -Path $ReportPath -Depth 7;&$save $report foreach($entry in @($Plan.Files)){ $path=[IO.Path]::GetFullPath([string]$entry.Path);$root=[IO.Path]::GetFullPath([string]$entry.Root).TrimEnd('\');$matches=@($catalog|Where-Object{[IO.Path]::GetFullPath([string]$_.Root).TrimEnd('\')-eq$root-and$_.Category-eq$entry.Category-and(Test-WindCleanupName ([IO.Path]::GetFileName($path)) @($_.Patterns))}) $out=[pscustomobject]@{Category=$entry.Category;Path=$path;Status='Checking';Reason=$null};$report.Entries+=@($out);&$save $report diff --git a/src/Wind.Hardware.psm1 b/src/Wind.Hardware.psm1 index 93cd61a..f2496b2 100644 --- a/src/Wind.Hardware.psm1 +++ b/src/Wind.Hardware.psm1 @@ -27,7 +27,7 @@ function Invoke-WindSensorApply { $plan=@(Get-WindSensorPlan -InstanceId $InstanceId);if(@($plan|Where-Object Status -eq 'AlreadyUnavailable').Count){throw 'One or more selected devices are already unavailable; select only enabled devices.'} if(-not $PSCmdlet.ShouldProcess('Selected PnP devices','Disable exact camera, microphone or sensor devices')){return $plan} $j=[pscustomobject]@{Schema=1;Kind='WindSensors';CreatedUtc=[DateTime]::UtcNow.ToString('o');Status='Prepared';Entries=@($plan|Where-Object Status -eq 'WouldDisable'|ForEach-Object{[pscustomobject]@{InstanceId=$_.InstanceId;Name=$_.Name;Status='Pending'}})} - $save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 7}.GetNewClosure();&$save $j + $save=New-MsPrivateJsonWriter -Path $JournalPath -Depth 7;&$save $j try{foreach($e in $j.Entries){$fresh=@(Get-WindSensorInventory|Where-Object InstanceId -ceq $e.InstanceId);if($fresh.Count-ne 1-or$fresh[0].Status-ne'OK'){throw "Device state changed: $($e.Name)"};$e.Status='Disabling';&$save $j;Disable-PnpDevice -InstanceId $e.InstanceId -Confirm:$false -ErrorAction Stop|Out-Null;$after=Get-PnpDevice -InstanceId $e.InstanceId -ErrorAction Stop;if($after.Status-eq'OK'){throw "Device still reports enabled after disable: $($e.Name)"};$e.Status='Disabled';&$save $j};$j.Status='Applied';&$save $j;$j}catch{$j.Status='ApplyFailed';&$save $j;throw} } diff --git a/src/Wind.Network.psm1 b/src/Wind.Network.psm1 index 16cbbdb..facbbd4 100644 --- a/src/Wind.Network.psm1 +++ b/src/Wind.Network.psm1 @@ -104,7 +104,7 @@ function Invoke-WindNetworkApply { $backup=$JournalPath+'.firewall.wfw' if($Profile-eq'Isolate'){&$Backend.ExportPolicy $backup} $journal=[pscustomobject]@{Schema=1;Kind='MsPrivateNetwork';CreatedUtc=[DateTime]::UtcNow.ToString('o');Profile=$Profile;Status='Prepared';RuleGroup=$catalog.RuleGroup;KeywordIds=@();Profiles=$before;RollbackTask=('ms-private Network Recovery '+[guid]::NewGuid().ToString('N'));BackupPath=if($Profile-eq'Isolate'){$backup}else{$null}} - $save={param($x) Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 8}.GetNewClosure() + $save=New-MsPrivateJsonWriter -Path $JournalPath -Depth 8 & $save $journal try{ if($Profile -eq 'BlockOptionalTelemetry'){ diff --git a/src/Wind.Recall.psm1 b/src/Wind.Recall.psm1 index 0c26b08..d953deb 100644 --- a/src/Wind.Recall.psm1 +++ b/src/Wind.Recall.psm1 @@ -40,7 +40,7 @@ function Invoke-WindRecallApply { if(-not $PSCmdlet.ShouldProcess('Windows Recall','Disable Recall and delete existing snapshots')){return $plan} $s=$plan.State $j=[pscustomobject]@{Schema=1;Kind='WindRecall';CreatedUtc=[DateTime]::UtcNow.ToString('o');Status='Prepared';Before=[pscustomobject]@{Policy=[pscustomobject]@{Exists=$s.PolicyExists;Kind=$s.PolicyKind;Value=$s.PolicyValue};FeatureState=$s.FeatureState};RemoveFeature=[bool]$RemoveFeature;Irreversible='Existing Recall snapshots deleted by policy cannot be restored.'} - $save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 7}.GetNewClosure();&$save $j + $save=New-MsPrivateJsonWriter -Path $JournalPath -Depth 7;&$save $j try{ Set-WindRecallPolicyState ([pscustomobject]@{Exists=$true;Value=1}) $after=Get-WindRecallState;if(-not $after.PolicyExists -or $after.PolicyValue -ne 1){throw 'Recall policy readback failed.'} diff --git a/tests/Run-FeatureTests.ps1 b/tests/Run-FeatureTests.ps1 index c9150e5..25463a2 100644 --- a/tests/Run-FeatureTests.ps1 +++ b/tests/Run-FeatureTests.ps1 @@ -29,6 +29,16 @@ try{ Assert-True ($invokerRun.Status-eq'Completed'-and[IO.File]::ReadAllText((Join-Path $ioRoot 'probe.txt'))-eq'nested-module') 'Captured child-script invoker failed across execution-module scope.' $threw=$false;try{& $invoker '..\outside.ps1' @{}|Out-Null}catch{$threw=$true};Assert-True $threw 'Child-script invoker accepted path traversal.' + $writerPath=Join-Path $ioRoot 'captured-writer.json';$writer=New-MsPrivateJsonWriter -Path $writerPath -Depth 4 + $writerAction=({& $writer ([pscustomobject]@{Value='captured-writer'})}.GetNewClosure()) + $writerRun=Invoke-MsPrivateGroupPlan -Groups @([pscustomobject]@{Id='Writer';Label='Captured JSON writer';DependsOn=@();Action=$writerAction}) + $writerSaved=Get-Content -LiteralPath $writerPath -Raw|ConvertFrom-Json + Assert-True ($writerRun.Status-eq'Completed'-and$writerSaved.Value-eq'captured-writer') 'Captured atomic JSON writer failed across execution-module scope.' + + $cleanupReport=Join-Path $ioRoot 'cleanup.json';$cleanupPlan=[pscustomobject]@{Schema=2;AgeDays=7;CutoffUtc=[DateTime]::UtcNow.AddDays(-7).ToString('o');Categories=@('Temp');Files=@()} + $cleanupResult=Invoke-WindCleanup -Plan $cleanupPlan -ReportPath $cleanupReport -Confirm:$false + Assert-True ($cleanupResult.Status-eq'Completed'-and(Test-Path -LiteralPath $cleanupReport)) 'Cleanup could not create and finalize its atomic report.' + $network=Get-Content -LiteralPath (Join-Path $root 'config/network.json') -Raw|ConvertFrom-Json;$network.OptionalTelemetry[1].Fqdn=$network.OptionalTelemetry[0].Fqdn;$path=Join-Path $ioRoot 'network.json';$network|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path $threw=$false;try{Get-WindNetworkCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Network catalog accepted a duplicate FQDN.' $network=Get-Content -LiteralPath (Join-Path $root 'config/network.json') -Raw|ConvertFrom-Json;$network.OptionalTelemetry[0].Fqdn='ctldl.windowsupdate.com';$path=Join-Path $ioRoot 'network-protected.json';$network|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path @@ -83,6 +93,8 @@ try{ $savedRun=Invoke-MsPrivateGroupPlan -Groups @([pscustomobject]@{Id='WriteReport';Label='Write report';DependsOn=@();Action={}}) -ReportPath $runReportPath $savedRunDisk=Get-Content -LiteralPath $runReportPath -Raw|ConvertFrom-Json Assert-True ($savedRun.Status-eq'Completed'-and$savedRunDisk.Status-eq'Completed'-and$savedRunDisk.Entries[0].Status-eq'Succeeded') 'Operation runner could not write and finalize a run report.' + $emptyRunPath=Join-Path $runReportRoot 'empty.json';$emptyRun=Invoke-MsPrivateGroupPlan -Groups @() -ReportPath $emptyRunPath + Assert-True ($emptyRun.Status-eq'Completed'-and@($emptyRun.Entries).Count-eq0-and(Test-Path -LiteralPath $emptyRunPath)) 'Empty operation plan did not complete as a clean no-op.' }finally{if(Test-Path -LiteralPath $runReportRoot){Remove-Item -LiteralPath $runReportRoot -Recurse -Force}} $calls=[Collections.Generic.List[string]]::new()