7 Commits
Author SHA1 Message Date
0x000 41bb252214 Release 0.0.6 with usable GUI diagnostics and capture controls
Windows tests / test-and-build (push) Canceled after 0s
2026-10-01 20:27:37 -04:00
0x000 65f302279d Validate packet capture outcomes and narrow cleanup inventory
Windows tests / test-and-build (push) Canceled after 0s
2026-09-26 21:24:03 -04:00
0x000 b9a3308214 Preserve cleanup previews and changed files
Windows tests / test-and-build (push) Canceled after 0s
2026-09-26 20:40:23 -04:00
0x000 0274e7b398 Fix deferred writers and release checksums
Windows tests / test-and-build (push) Canceled after 0s
2026-09-24 13:55:18 -04:00
0x000 4dfec83063 Fix GUI operation script scope
Windows tests / test-and-build (push) Canceled after 0s
2026-09-24 13:19:44 -04:00
0x000 cdfa0ecda8 Fix GUI apply status update
Windows tests / test-and-build (push) Canceled after 0s
2026-09-23 19:25:04 -04:00
0x000 22d06ac7fd Fix run reports and Windows Home policy planning
Windows tests / test-and-build (push) Canceled after 0s
2026-09-23 18:49:13 -04:00
34 changed files with 392 additions and 96 deletions

No files matched your search

+4 -4
View File
@@ -21,11 +21,11 @@ jobs:
dotnet-version: 10.0.x dotnet-version: 10.0.x
- name: Build executable - name: Build executable
shell: powershell shell: powershell
run: .\Build-Executable.ps1 -OutputDirectory .\dist\ci -Version 0.0.1 run: .\Build-Executable.ps1 -OutputDirectory .\dist\ci -Version 0.0.6
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: ms-private-0.0.1 name: ms-private-0.0.6
path: | path: |
dist/ci/ms-private-0.0.1.exe dist/ci/ms-private-0.0.6.exe
dist/ci/ms-private-0.0.1.exe.sha256 dist/ci/ms-private-0.0.6.exe.sha256
dist/ci/SHA256SUMS.txt dist/ci/SHA256SUMS.txt
+1 -1
View File
@@ -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.'} if(-not$AcknowledgeAccountDataDeletion){throw 'Apply requires -AcknowledgeAccountDataDeletion. App data, tokens and matching stored credentials cannot be restored by ms-private.'}
Assert-WindMutationAllowed Assert-WindMutationAllowed
if(-not$PSCmdlet.ShouldProcess('Current-user Microsoft application accounts and matching stored credentials','Permanently sign out and delete local account data')){return} 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)"} 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.' $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.'
+3 -2
View File
@@ -1,5 +1,5 @@
#Requires -Version 5.1 #Requires -Version 5.1
[CmdletBinding()]param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.1') [CmdletBinding()]param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.6')
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop' 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 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 $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} 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.'} $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} 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}} }finally{if(Test-Path -LiteralPath $stage){Remove-Item -LiteralPath $stage -Recurse -Force}}
+1 -1
View File
@@ -1,5 +1,5 @@
#Requires -Version 5.1 #Requires -Version 5.1
[CmdletBinding()]param([string]$OutputDirectory,[string]$Version='0.0.1') [CmdletBinding()]param([string]$OutputDirectory,[string]$Version='0.0.6')
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop' Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
if($Version-notmatch'^\d+\.\d+\.\d+$'){throw 'Version must use major.minor.patch.'} 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 $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
+3 -2
View File
@@ -1,6 +1,6 @@
#Requires -Version 5.1 #Requires -Version 5.1
[CmdletBinding()] [CmdletBinding()]
param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.1') param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.6')
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference='Stop' $ErrorActionPreference='Stop'
if (-not $OutputDirectory) { $OutputDirectory=Join-Path $PSScriptRoot 'dist' } if (-not $OutputDirectory) { $OutputDirectory=Join-Path $PSScriptRoot 'dist' }
@@ -25,5 +25,6 @@ try {
} }
} finally {$zip.Dispose()} } finally {$zip.Dispose()}
$hash=(Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant() $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" Write-Output "Created $zipPath ($($files.Count) source files). SHA256: $hash"
+1
View File
@@ -4,6 +4,7 @@ Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join
$Category=@($Category|ForEach-Object{$_.Split(',')}|ForEach-Object{$_.Trim()}|Where-Object{$_}|Select-Object -Unique);$valid=@('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports');if(@($Category|Where-Object{$_-notin$valid}).Count){throw 'Unknown cleanup category.'} $Category=@($Category|ForEach-Object{$_.Split(',')}|ForEach-Object{$_.Trim()}|Where-Object{$_}|Select-Object -Unique);$valid=@('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports');if(@($Category|Where-Object{$_-notin$valid}).Count){throw 'Unknown cleanup category.'}
$plan=if($Mode-in@('Plan','Run')){Get-WindCleanupPlan -AgeDays $AgeDays -Category $Category}else{$null} $plan=if($Mode-in@('Plan','Run')){Get-WindCleanupPlan -AgeDays $AgeDays -Category $Category}else{$null}
if($Mode-eq'Plan'){if($Json){$plan|ConvertTo-Json -Depth 7}else{$plan|Select-Object FileCount,Bytes,AgeDays,Exclusions|Format-List;$plan.Files|Select-Object Path,Length,LastWriteUtc|Format-Table -Wrap};return} if($Mode-eq'Plan'){if($Json){$plan|ConvertTo-Json -Depth 7}else{$plan|Select-Object FileCount,Bytes,AgeDays,Exclusions|Format-List;$plan.Files|Select-Object Path,Length,LastWriteUtc|Format-Table -Wrap};return}
if($Mode-eq'Run'-and-not$PSCmdlet.ShouldProcess('Selected cleanup files and optional current clipboard','Delete planned files and clear clipboard when requested')){return}
$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\cleanup';[IO.Directory]::CreateDirectory($root)|Out-Null $root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\cleanup';[IO.Directory]::CreateDirectory($root)|Out-Null
if($Mode-eq'Run'){$report=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false|Format-List;if($ClearClipboard){Clear-WindCurrentClipboard;Write-Output 'Current clipboard cleared.'};Write-Output "Cleanup report: $report";return} if($Mode-eq'Run'){$report=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false|Format-List;if($ClearClipboard){Clear-WindCurrentClipboard;Write-Output 'Current clipboard cleared.'};Write-Output "Cleanup report: $report";return}
Assert-WindMutationAllowed Assert-WindMutationAllowed
+1 -1
View File
@@ -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} 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 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($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
+20 -10
View File
@@ -1,7 +1,7 @@
#Requires -Version 5.1 #Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
param( param(
[ValidateSet('Plan','Apply','Restore','Confirm','Validate','CaptureStart','CaptureStop')][string]$Mode='Plan', [ValidateSet('Plan','Apply','Restore','Confirm','Validate','CaptureStart','CaptureStop','CaptureConvert')][string]$Mode='Plan',
[ValidateSet('Observe','BlockOptionalTelemetry','Isolate')][string]$Profile='Observe', [ValidateSet('Observe','BlockOptionalTelemetry','Isolate')][string]$Profile='Observe',
[int]$RollbackMinutes=10,[string]$Journal,[string]$CaptureDirectory,[switch]$FromRecoveryTask,[switch]$Json [int]$RollbackMinutes=10,[string]$Journal,[string]$CaptureDirectory,[switch]$FromRecoveryTask,[switch]$Json
) )
@@ -12,19 +12,29 @@ $stateRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsP
if($Mode -eq 'Plan'){$x=Get-WindNetworkPlan -Profile $Profile -RollbackMinutes $RollbackMinutes;if($Json){$x|ConvertTo-Json -Depth 8}else{$x.Items|Format-Table -Wrap};return} if($Mode -eq 'Plan'){$x=Get-WindNetworkPlan -Profile $Profile -RollbackMinutes $RollbackMinutes;if($Json){$x|ConvertTo-Json -Depth 8}else{$x.Items|Format-Table -Wrap};return}
if($Mode -eq 'Validate'){$x=Get-WindNetworkValidation;if($Json){$x|ConvertTo-Json -Depth 8}else{$x|Format-List};return} if($Mode -eq 'Validate'){$x=Get-WindNetworkValidation;if($Json){$x|ConvertTo-Json -Depth 8}else{$x|Format-List};return}
if($Mode -eq 'CaptureStart'){ if($Mode -eq 'CaptureStart'){
Assert-WindMutationAllowed;if(-not (Get-Command pktmon.exe -ErrorAction SilentlyContinue)){throw 'pktmon.exe is unavailable.'} if(-not $CaptureDirectory){$CaptureDirectory=Join-Path $stateRoot ('capture-'+[guid]::NewGuid().ToString('N'))}
if(-not $CaptureDirectory){$CaptureDirectory=Join-Path $stateRoot ('capture-'+(Get-Date -Format 'yyyyMMdd-HHmmss'))} $CaptureDirectory=Resolve-WindCaptureDirectory -Path $CaptureDirectory -StateRoot $stateRoot
$etl=Join-Path $CaptureDirectory 'capture.etl';if(Test-Path -LiteralPath $etl){throw 'Capture output already exists. Choose a new capture directory.'}
if(-not $PSCmdlet.ShouldProcess($CaptureDirectory,'Start packet capture')){return}
Assert-WindMutationAllowed
[IO.Directory]::CreateDirectory($CaptureDirectory)|Out-Null [IO.Directory]::CreateDirectory($CaptureDirectory)|Out-Null
$etl=Join-Path $CaptureDirectory 'capture.etl';& pktmon.exe start --capture --comp nics --pkt-size 0 --file-name $etl|Out-Host Invoke-WindPacketMonitor -ArgumentList @('start','--capture','--comp','nics','--pkt-size','0','--file-name',$etl)
[pscustomobject]@{Status='Running';Directory=$CaptureDirectory;Warning='Capture can contain private addresses and DNS names. Stop and delete or export it explicitly.'}|ConvertTo-Json [pscustomobject]@{Status='Running';Directory=$CaptureDirectory;Warning='Capture can contain private addresses and DNS names. Stop and delete or export it explicitly.'}|ConvertTo-Json
return return
} }
if($Mode -eq 'CaptureStop'){ if($Mode -in @('CaptureStop','CaptureConvert')){
if(-not $CaptureDirectory){throw 'CaptureStop requires -CaptureDirectory.'};$full=[IO.Path]::GetFullPath($CaptureDirectory) if(-not $CaptureDirectory){throw "$Mode requires -CaptureDirectory."};$full=Resolve-WindCaptureDirectory -Path $CaptureDirectory -StateRoot $stateRoot
if(-not $full.StartsWith([IO.Path]::GetFullPath($stateRoot)+'\',[StringComparison]::OrdinalIgnoreCase)){throw 'Capture directory must be inside the ms-private local state directory.'} $action=if($Mode-eq'CaptureStop'){'Stop packet capture and convert to PCAPNG'}else{'Convert saved packet capture to PCAPNG'}
& pktmon.exe stop|Out-Host;$etl=Join-Path $full 'capture.etl';$pcap=Join-Path $full 'capture.pcapng';if(Test-Path $etl){& pktmon.exe etl2pcap $etl --out $pcap|Out-Host} if(-not $PSCmdlet.ShouldProcess($full,$action)){return}
Assert-WindMutationAllowed
$etl=Join-Path $full 'capture.etl';$pcap=Join-Path $full 'capture.pcapng'
if(-not(Test-Path -LiteralPath $etl -PathType Leaf)){throw 'Capture ETL file is missing; no capture was stopped.'}
if(Test-Path -LiteralPath $pcap){throw 'Converted capture already exists; preserve or move it before retrying conversion.'}
if($Mode-eq'CaptureStop'){Invoke-WindPacketMonitor -ArgumentList @('stop')}
Invoke-WindPacketMonitor -ArgumentList @('etl2pcap',$etl,'--out',$pcap)
if(-not(Test-Path -LiteralPath $pcap -PathType Leaf)){throw 'Packet capture conversion returned success without creating the PCAPNG file.'}
Save-WindJournal -Journal (Get-WindNetworkValidation) -Path (Join-Path $full 'connections.json') Save-WindJournal -Journal (Get-WindNetworkValidation) -Path (Join-Path $full 'connections.json')
Write-Output "Capture stopped: $full";return Write-Output "Capture converted: $pcap";return
} }
Assert-WindMutationAllowed Assert-WindMutationAllowed
if($Mode -eq 'Apply'){ if($Mode -eq 'Apply'){
@@ -40,5 +50,5 @@ if($Mode -eq 'Apply'){
} }
if(-not $Journal){throw "$Mode requires -Journal."} if(-not $Journal){throw "$Mode requires -Journal."}
$saved=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json $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} if($Mode-eq'Confirm'){Confirm-WindNetwork -Journal $saved -Save $save|Format-List}else{Restore-WindNetwork -Journal $saved -Save $save|Format-List}
+5 -3
View File
@@ -23,7 +23,7 @@
## Run ## Run
Download `ms-private-0.0.1.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.6.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\<version>-<payload hash>`, 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. 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\<version>-<payload hash>`, 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.
@@ -146,7 +146,7 @@ WinGet must be installed and its `winget` source usable. ms-private retains norm
## Support and recovery ## Support and recovery
- Policy apply/restore: Windows 11 Pro/Enterprise/Education families, builds 26100/26200. Home and unreviewed builds use the manual guide; the menu explicitly reports skipped policies. - Policy apply/restore: Windows 11 Pro/Enterprise/Education families, builds 26100/26200. On Home, seven Microsoft-documented direct registry controls remain available; managed-edition CSP policies are explicitly skipped and linked to the manual guide. Unreviewed builds remain audit-only.
- App operations: build gate 19045/22631/26100/26200. Windows 10 requires an appropriately supported servicing arrangement. Build gating is not proof of successful live testing. - App operations: build gate 19045/22631/26100/26200. Windows 10 requires an appropriately supported servicing arrangement. Build gating is not proof of successful live testing.
- Domain/MDM-managed devices and non-elevated changes are refused. Use an administrator-managed deployment for those devices. - Domain/MDM-managed devices and non-elevated changes are refused. Use an administrator-managed deployment for those devices.
- Policy changes save original values before writing and roll back failures. Restoration preserves later conflicting edits. Restore newer journals before older ones. - Policy changes save original values before writing and roll back failures. Restoration preserves later conflicting edits. Restore newer journals before older ones.
@@ -179,12 +179,14 @@ Read the [complete tool breakdown](docs/TOOL-BREAKDOWN.md), [current roadmap](do
## Development and release status ## Development and release status
Version 0.0.6 is a prerelease containing cleanup preview, stale-file, dependency-matching, packet-capture and GUI output fixes. The executable uses Windows PowerShell 5.1, which passes 144 checks plus the GUI construction smoke test. PowerShell 7 cleanup remains blocked by antivirus on the development host; it is not validated for cleanup. See [validation status](docs/VALIDATION.md). No security exclusions or bypasses are installed.
```powershell ```powershell
.\tests\Run-All.ps1 .\tests\Run-All.ps1
.\Build-Executable.ps1 .\Build-Executable.ps1
.\Build-Release.ps1 .\Build-Release.ps1
``` ```
Tests use synthetic registry/app/network/service/account backends, read-only security inventory and temporary workspace files. CI runs 114 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 is configured to run 144 PowerShell checks on Windows PowerShell 5.1 and PowerShell 7, plus ten native launcher checks, PE inspection and the one-file publish. Run `tests/Run-GuiSmokeTest.ps1` for a separate GUI construction check without invoking window actions. 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. `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.
+1 -1
View File
@@ -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} 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 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($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
+2 -1
View File
@@ -7,5 +7,6 @@ Assert-WindMutationAllowed
$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\services';[IO.Directory]::CreateDirectory($root)|Out-Null $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($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(-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" $journal|Select-Object Kind,Status,CreatedUtc|Format-List;Write-Output "Service recovery journal: $Journal"
+1 -1
View File
@@ -6,7 +6,7 @@
<Title>ms-private</Title> <Title>ms-private</Title>
<Description>Launches the ms-private graphical wizard. ms-private preserves Defender, Windows Firewall, Windows Update, security logs, WebView2, drivers and runtimes.</Description> <Description>Launches the ms-private graphical wizard. ms-private preserves Defender, Windows Firewall, Windows Update, security logs, WebView2, drivers and runtimes.</Description>
<Details>Every selected control is previewed and applied by the same PowerShell engine used by the standalone package.</Details> <Details>Every selected control is previewed and applied by the same PowerShell engine used by the standalone package.</Details>
<Version>0.0.1</Version> <Version>0.0.6</Version>
<UniqueId>64d76b08-f58d-499c-aadc-1d72d620e704</UniqueId> <UniqueId>64d76b08-f58d-499c-aadc-1d72d620e704</UniqueId>
<Overhaul>false</Overhaul> <Overhaul>false</Overhaul>
<SupportedBuilds> <SupportedBuilds>
+7
View File
@@ -262,6 +262,7 @@
"Standard": 1, "Standard": 1,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings", "Source": "https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings",
"Impact": "Stops Windows Error Reporting. Automatic crash analysis and solution lookup are unavailable; local security event channels are unaffected.", "Impact": "Stops Windows Error Reporting. Automatic crash analysis and solution lookup are unavailable; local security event channels are unaffected.",
"Exceptions": [] "Exceptions": []
@@ -274,6 +275,7 @@
"Standard": 1, "Standard": 1,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings", "Source": "https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings",
"Impact": "Adds a defense-in-depth restriction on second-level WER data if another component re-enables reporting.", "Impact": "Adds a defense-in-depth restriction on second-level WER data if another component re-enables reporting.",
"Exceptions": [] "Exceptions": []
@@ -348,6 +350,7 @@
"Standard": 1, "Standard": 1,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services", "Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services",
"Impact": "Restricts typing personalization collection for the current user. It does not block input observed by applications receiving keystrokes.", "Impact": "Restricts typing personalization collection for the current user. It does not block input observed by applications receiving keystrokes.",
"Exceptions": [] "Exceptions": []
@@ -361,6 +364,7 @@
"Standard": 1, "Standard": 1,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services", "Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services",
"Impact": "Restricts handwriting personalization collection for the current user; recognition and personalization quality can be reduced.", "Impact": "Restricts handwriting personalization collection for the current user; recognition and personalization quality can be reduced.",
"Exceptions": [] "Exceptions": []
@@ -409,6 +413,7 @@
"Standard": 0, "Standard": 0,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services", "Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services",
"Impact": "Strict stops device metadata downloads; device icons, names or companion information can be less complete.", "Impact": "Strict stops device metadata downloads; device icons, names or companion information can be less complete.",
"Exceptions": [] "Exceptions": []
@@ -421,6 +426,7 @@
"Standard": 1, "Standard": 1,
"Strict": 0, "Strict": 0,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services", "Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services",
"Impact": "Strict prevents Windows from downloading missing fonts; documents or sites can render with fallback fonts.", "Impact": "Strict prevents Windows from downloading missing fonts; documents or sites can render with fallback fonts.",
"Exceptions": [] "Exceptions": []
@@ -433,6 +439,7 @@
"Standard": 1, "Standard": 1,
"Strict": 1, "Strict": 1,
"EnterpriseValue": null, "EnterpriseValue": null,
"HomeSupported": true,
"Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services", "Source": "https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services",
"Impact": "Stops MSRT infection reports while preserving the tool and its delivery through Windows Update.", "Impact": "Stops MSRT infection reports while preserving the tool and its delivery through Windows Update.",
"Exceptions": [] "Exceptions": []
+1 -1
View File
@@ -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. 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.1. 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.6. 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. 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.
+2 -2
View File
@@ -1,6 +1,6 @@
# ms-private 0.0.1 roadmap # ms-private 0.0.6 roadmap
Version 0.0.1 is an unpublished release candidate. 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, release provenance and the separately designed offline-maintenance system. Version 0.0.6 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 ## Open work
+6 -4
View File
@@ -1,4 +1,6 @@
# Delivery status — 2026-09-19 # Delivery status - 2026-10-01
Version 0.0.6 packages the cleanup preview, stale-file, dependency and packet-capture fixes since v0.0.5. The GUI now displays security audit results, exposes capture start/stop/conversion and the capture path, and opens Home Settings without relying on PowerShell file associations. Windows PowerShell 5.1 passes 144 checks plus the GUI construction smoke check. The executable continues to use Windows PowerShell 5.1. PowerShell 7 cleanup is still blocked by an antivirus response on this host; the responsible security product has not been established. This is a prerelease with that known compatibility limitation, not a claim of full live validation.
The update architecture and implementation pass was completed on 2026-09-20. The product is named ms-private. Local OS inventory, offline bundle verification, the graphical and terminal wizards, app/debloat flows, Microsoft app-account purge, network enforcement and capture, reversible privacy-service shutdown, Recall, exact sensors, privacy-artifact cleanup, lock handling, security status and AME packaging are implemented or prepared as described below. No real package download, app removal, account purge, policy write, service mutation, firewall change, device disable, cleanup or Recall deletion was performed. The update architecture and implementation pass was completed on 2026-09-20. The product is named ms-private. Local OS inventory, offline bundle verification, the graphical and terminal wizards, app/debloat flows, Microsoft app-account purge, network enforcement and capture, reversible privacy-service shutdown, Recall, exact sensors, privacy-artifact cleanup, lock handling, security status and AME packaging are implemented or prepared as described below. No real package download, app removal, account purge, policy write, service mutation, firewall change, device disable, cleanup or Recall deletion was performed.
@@ -23,15 +25,15 @@ 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. - 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. - 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. - AME wrapper source and builder that launch the same graphical wizard without disabling Defender or Windows Update.
- Self-contained x64 `ms-private-0.0.1.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.6.exe` launcher with a `requireAdministrator` manifest, embedded hash-manifested runtime, exact-file verification, tamper repair and protected Program Files installation.
## Verification performed ## Verification performed
- 114 tests pass in PowerShell 7 and Windows PowerShell 5.1: 25 policy/transaction, 29 app/menu, 19 offline-bundle and 41 feature/package/security/service/account/preset/operation-runner checks. - 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. - 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. - Shared journal/report create, flush, atomic-replace and parse tests passed using temporary workspace files.
- All PowerShell source files parsed successfully during verification. - All PowerShell source files parsed successfully during verification.
- Real read-only service planning completed in both PowerShell engines. Policy enforcement remains audit-only on this Windows Home (`Core`) build 26200. - Real read-only service and policy planning completed in both PowerShell engines. On this Windows Home (`Core`) build 26200, seven documented direct registry controls are eligible and 45 managed-edition policies remain unsupported.
- Installer previews used `-WhatIf`; no package installation occurred. - Installer previews used `-WhatIf`; no package installation occurred.
- Live operating-system policy mutation, app uninstallation, firewall changes and telemetry suppression were not performed or claimed. - Live operating-system policy mutation, app uninstallation, firewall changes and telemetry suppression were not performed or claimed.
+3 -3
View File
@@ -6,7 +6,7 @@ ms-private is a local Windows privacy and debloat toolkit. It builds an exact pl
| Interface | Entry point | Use | | Interface | Entry point | Use |
| --- | --- | --- | | --- | --- | --- |
| Single-file Windows release | `ms-private-0.0.1.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.6.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. | | 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. | | 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. | | Complete preset CLI | `Preset.ps1` | Auto-detect or plan/apply EstablishedSafe, FreshInstall, Gentle, Medium, Strong or Maximum without navigating the UI. |
@@ -31,7 +31,7 @@ The policy catalog contains 56 reviewed controls. Standard and Strict cover diag
Update choices are separate: Keep, Automatic, Notify and Manual. The update engine, Update Orchestrator, BITS, Cryptographic Services and servicing infrastructure remain installed. Update choices are separate: Keep, Automatic, Notify and Manual. The update engine, Update Orchestrator, BITS, Cryptographic Services and servicing infrastructure remain installed.
The engine checks Windows edition and build, refuses domain or MDM-managed devices, journals original values before writing, reads back changes, rolls back partial failures and preserves later conflicting edits during restoration. Home and unreviewed builds receive an explicit unsupported result and the Settings guide. The engine checks Windows edition and build, refuses domain or MDM-managed devices, journals original values before writing, reads back changes, rolls back partial failures and preserves later conflicting edits during restoration. Home can apply seven direct registry controls documented independently of managed-edition CSP support; its remaining CSP policies are explicitly unsupported and linked to the Settings guide. Unreviewed builds remain audit-only.
## Services and GDID-related reduction ## Services and GDID-related reduction
@@ -107,7 +107,7 @@ Reports are local and have no automatic upload. Installed-app inventories and ne
## Running a reviewed download ## Running a reviewed download
For a normal release, verify `ms-private-0.0.1.exe.sha256` and launch `ms-private-0.0.1.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.6.exe.sha256` and launch `ms-private-0.0.6.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. 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.
+12 -2
View File
@@ -2,7 +2,17 @@
## Completed checks ## Completed checks
Release-candidate update, 2026-09-22: 114 tests pass in each of Windows PowerShell 5.1 and PowerShell 7 (25 policy/transaction, 29 app/menu, 19 offline-bundle, 41 feature/package/security/service/account/preset/operation-runner). The policy tests include current-user registry scope 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 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. Prerelease 0.0.6, 2026-10-01: Windows PowerShell 5.1 passes 144 checks (25 policy, 29 app/menu, 19 offline, 49 feature, 7 cleanup and 15 capture). A separate GUI construction smoke test passes without opening a window or invoking actions. Capture conversion can be retried separately with CaptureConvert; the GUI shows its path and the security audit output. PowerShell 7 cleanup still reports a security-product block. The executable uses Windows PowerShell 5.1 as before; no protection settings were changed.
Unreleased audit, 2026-09-26: seven cleanup regressions pass in Windows PowerShell 5.1, covering a side-effect-free preview, unchanged-file deletion, timestamp and length changes, locked files, invalid timestamps and the real cleanup entry script with a mocked clipboard. The file tests use the production allowlist and plans containing only disposable files. The operation runner also tests differently capitalized dependency names. Fourteen packet-capture checks pass in both engines, covering path boundaries, file ancestors, argument preservation, native-command failures, absent exit codes and previews through the real entry script. The Windows PowerShell 5.1 component suites total 142 passing checks.
PowerShell 7 still reports `ScriptContainedMaliciousContent` in production `Get-WindBrowserCacheCatalog` enumeration and `Invoke-WindCleanup` target validation. The block reproduced after removing the original private-function test replacement, so the earlier test-mock-only diagnosis was incomplete. Both Defender and Bitdefender are registered on the host; available Defender events did not identify the source. No security exclusions or bypasses were added. This blocks a full compatibility claim; the 0.0.6 prerelease explicitly retains the limitation and uses the existing Windows PowerShell 5.1 launcher. Changed-file detection uses size and modification time; it does not detect identical metadata or eliminate the filesystem race between checking and deleting a file. Capture path checks likewise do not eliminate concurrent filesystem replacement; the global Pktmon session still requires live ownership/interruption testing.
All 48 feature tests also pass in PowerShell 7. The previous v0.0.5 Gitea job ended as `Canceled after 0s`; it provides no remote test evidence. The local validation results above remain distinct from server CI status.
Cleanup now enumerates browser profiles only when BrowserCache is selected. Packet-monitor invocation was also checked against the real `pktmon help` command; this is a read-only native-command smoke test, not evidence of successful live capture or conversion.
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. 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.
@@ -18,7 +28,7 @@ Run `./tests/Run-All.ps1` with both PowerShell 7 and Windows PowerShell 5.1. Thi
| OS | Test | | OS | Test |
| --- | --- | | --- | --- |
| Windows 11 Home 24H2/25H2 | Audit reports unsupported machine policies; Apply makes no changes; manual guide matches current Settings | | Windows 11 Home 24H2/25H2 | Seven documented direct registry controls apply transactionally; managed-edition policies remain unsupported; manual guide matches current Settings |
| Windows 11 Pro 24H2/25H2 | Standard/Strict policy and service apply, readback, reboot, behavioral checks and restore | | Windows 11 Pro 24H2/25H2 | Standard/Strict policy and service apply, readback, reboot, behavioral checks and restore |
| Windows 11 Enterprise/Education 24H2/25H2 | Same, including actual diagnostic policy effect | | Windows 11 Enterprise/Education 24H2/25H2 | Same, including actual diagnostic policy effect |
| Unknown/future build | No automatic mutation until separately reviewed | | Unknown/future build | No automatic mutation until separately reviewed |
+38 -25
View File
@@ -4,13 +4,14 @@ Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
if([Environment]::OSVersion.Platform-ne[PlatformID]::Win32NT){throw 'Windows is required.'} if([Environment]::OSVersion.Platform-ne[PlatformID]::Win32NT){throw 'Windows is required.'}
Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Hardware.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Apps.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Recall.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Presets.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Execution.psm1') Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Hardware.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Apps.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Recall.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Presets.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Execution.psm1')
$windowsContext=Get-WindContext
function Add-WindCheckBox {param($Parent,[string]$Text,[int]$X,[int]$Y,[bool]$Checked=$false,[int]$Width=520);$c=[Windows.Forms.CheckBox]::new();$c.Text=$Text;$c.Location=[Drawing.Point]::new($X,$Y);$c.Size=[Drawing.Size]::new($Width,26);$c.Checked=$Checked;$Parent.Controls.Add($c);$c} function Add-WindCheckBox {param($Parent,[string]$Text,[int]$X,[int]$Y,[bool]$Checked=$false,[int]$Width=520);$c=[Windows.Forms.CheckBox]::new();$c.Text=$Text;$c.Location=[Drawing.Point]::new($X,$Y);$c.Size=[Drawing.Size]::new($Width,26);$c.Checked=$Checked;$Parent.Controls.Add($c);$c}
function Add-WindLabel {param($Parent,[string]$Text,[int]$X,[int]$Y,[int]$Width=600,[int]$Height=40);$l=[Windows.Forms.Label]::new();$l.Text=$Text;$l.Location=[Drawing.Point]::new($X,$Y);$l.Size=[Drawing.Size]::new($Width,$Height);$Parent.Controls.Add($l);$l} function Add-WindLabel {param($Parent,[string]$Text,[int]$X,[int]$Y,[int]$Width=600,[int]$Height=40);$l=[Windows.Forms.Label]::new();$l.Text=$Text;$l.Location=[Drawing.Point]::new($X,$Y);$l.Size=[Drawing.Size]::new($Width,$Height);$Parent.Controls.Add($l);$l}
function Invoke-WindChild {param([string]$Script,[hashtable]$Parameters=@{});& (Join-Path $PSScriptRoot $Script) @Parameters|Out-Host} $invokeWindChild=New-MsPrivateScriptInvoker -Root $PSScriptRoot
$form=[Windows.Forms.Form]::new();$form.Text='ms-private';$form.Size=[Drawing.Size]::new(860,690);$form.MinimumSize=[Drawing.Size]::new(760,600);$form.StartPosition='CenterScreen';$form.Font=[Drawing.Font]::new('Segoe UI',10);$form.BackColor=[Drawing.Color]::FromArgb(245,247,250) $form=[Windows.Forms.Form]::new();$form.Text='ms-private';$form.Size=[Drawing.Size]::new(860,690);$form.MinimumSize=[Drawing.Size]::new(760,600);$form.StartPosition='CenterScreen';$form.Font=[Drawing.Font]::new('Segoe UI',10);$form.BackColor=[Drawing.Color]::FromArgb(245,247,250)
$title=Add-WindLabel $form 'ms-private · Windows privacy control center' 22 15 790 35;$title.Font=[Drawing.Font]::new('Segoe UI Semibold',16);$title.ForeColor=[Drawing.Color]::FromArgb(20,65,110) $title=Add-WindLabel $form 'ms-private - Windows privacy control center' 22 15 790 35;$title.Font=[Drawing.Font]::new('Segoe UI Semibold',16);$title.ForeColor=[Drawing.Color]::FromArgb(20,65,110)
$tabs=[Windows.Forms.TabControl]::new();$tabs.Location=[Drawing.Point]::new(20,58);$tabs.Size=[Drawing.Size]::new(804,510);$form.Controls.Add($tabs) $tabs=[Windows.Forms.TabControl]::new();$tabs.Location=[Drawing.Point]::new(20,58);$tabs.Size=[Drawing.Size]::new(804,510);$form.Controls.Add($tabs)
$privacy=[Windows.Forms.TabPage]::new('Privacy');$apps=[Windows.Forms.TabPage]::new('Apps & browsers');$network=[Windows.Forms.TabPage]::new('Network');$devices=[Windows.Forms.TabPage]::new('Devices');$maintenance=[Windows.Forms.TabPage]::new('Maintenance');$homeTab=[Windows.Forms.TabPage]::new('Home guide');$tabs.TabPages.AddRange(@($privacy,$apps,$network,$devices,$maintenance,$homeTab)) $privacy=[Windows.Forms.TabPage]::new('Privacy');$apps=[Windows.Forms.TabPage]::new('Apps & browsers');$network=[Windows.Forms.TabPage]::new('Network');$devices=[Windows.Forms.TabPage]::new('Devices');$maintenance=[Windows.Forms.TabPage]::new('Maintenance');$homeTab=[Windows.Forms.TabPage]::new('Home guide');$tabs.TabPages.AddRange(@($privacy,$apps,$network,$devices,$maintenance,$homeTab))
@@ -39,7 +40,13 @@ Add-WindLabel $network 'Observe changes nothing. Optional telemetry blocks only
$netProfile=[Windows.Forms.ComboBox]::new();$netProfile.DropDownStyle='DropDownList';$netProfile.Items.AddRange(@('Observe','BlockOptionalTelemetry','Isolate'));$netProfile.SelectedIndex=0;$netProfile.Location=[Drawing.Point]::new(24,85);$netProfile.Width=280;$network.Controls.Add($netProfile) $netProfile=[Windows.Forms.ComboBox]::new();$netProfile.DropDownStyle='DropDownList';$netProfile.Items.AddRange(@('Observe','BlockOptionalTelemetry','Isolate'));$netProfile.SelectedIndex=0;$netProfile.Location=[Drawing.Point]::new(24,85);$netProfile.Width=280;$network.Controls.Add($netProfile)
$rollback=[Windows.Forms.NumericUpDown]::new();$rollback.Minimum=2;$rollback.Maximum=60;$rollback.Value=10;$rollback.Location=[Drawing.Point]::new(330,85);$network.Controls.Add($rollback);Add-WindLabel $network 'minutes to automatic rollback' 460 87 240 28|Out-Null $rollback=[Windows.Forms.NumericUpDown]::new();$rollback.Minimum=2;$rollback.Maximum=60;$rollback.Value=10;$rollback.Location=[Drawing.Point]::new(330,85);$network.Controls.Add($rollback);Add-WindLabel $network 'minutes to automatic rollback' 460 87 240 28|Out-Null
$capture=[Windows.Forms.Button]::new();$capture.Text='Start local packet capture';$capture.Location=[Drawing.Point]::new(24,140);$capture.Size=[Drawing.Size]::new(250,38);$network.Controls.Add($capture) $capture=[Windows.Forms.Button]::new();$capture.Text='Start local packet capture';$capture.Location=[Drawing.Point]::new(24,140);$capture.Size=[Drawing.Size]::new(250,38);$network.Controls.Add($capture)
$capture.Add_Click({try{Invoke-WindChild 'Network.ps1' @{Mode='CaptureStart'};[Windows.Forms.MessageBox]::Show('Capture started. Use Network.ps1 -Mode CaptureStop -CaptureDirectory <shown path> to stop and convert it.','ms-private')|Out-Null}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}}) $capturePath=[Windows.Forms.TextBox]::new();$capturePath.Location=[Drawing.Point]::new(24,195);$capturePath.Width=720;$network.Controls.Add($capturePath)
Add-WindLabel $network 'Capture directory (copy this path to reopen or convert a saved capture)' 24 226 730 30|Out-Null
$stopCapture=[Windows.Forms.Button]::new();$stopCapture.Text='Stop and convert';$stopCapture.Location=[Drawing.Point]::new(285,140);$stopCapture.Size=[Drawing.Size]::new(210,38);$network.Controls.Add($stopCapture)
$convertCapture=[Windows.Forms.Button]::new();$convertCapture.Text='Convert saved capture';$convertCapture.Location=[Drawing.Point]::new(506,140);$convertCapture.Size=[Drawing.Size]::new(238,38);$network.Controls.Add($convertCapture)
$capture.Add_Click({try{$result=& $invokeWindChild 'Network.ps1' @{Mode='CaptureStart'} -PassThru;$captureResult=$result|ConvertFrom-Json;$capturePath.Text=[string]$captureResult.Directory;[Windows.Forms.MessageBox]::Show("Capture started. Use Stop and convert when finished.`r`n`r`n$($capturePath.Text)",'ms-private')|Out-Null}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}})
foreach($button in @($stopCapture,$convertCapture)){$button.Tag=if($button-eq$stopCapture){'CaptureStop'}else{'CaptureConvert'};$button.Add_Click({param($sender,$eventArgs)try{if([string]::IsNullOrWhiteSpace($capturePath.Text)){throw 'Enter the capture directory first.'};& $invokeWindChild 'Network.ps1' @{Mode=[string]$sender.Tag;CaptureDirectory=$capturePath.Text};[Windows.Forms.MessageBox]::Show("Saved capture.pcapng in:`r`n$($capturePath.Text)",'ms-private')|Out-Null}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}})}
Add-WindLabel $network 'Capture continues until stopped, including after this window closes. Pktmon has one system-wide session; do not use these controls while another tool owns a capture.' 24 270 730 68|Out-Null
Add-WindLabel $devices 'Select exact hardware devices to disable. Nothing is selected by default.' 20 18 700 28|Out-Null Add-WindLabel $devices 'Select exact hardware devices to disable. Nothing is selected by default.' 20 18 700 28|Out-Null
$deviceList=[Windows.Forms.CheckedListBox]::new();$deviceList.Location=[Drawing.Point]::new(24,58);$deviceList.Size=[Drawing.Size]::new(720,340);$deviceList.CheckOnClick=$true;$devices.Controls.Add($deviceList) $deviceList=[Windows.Forms.CheckedListBox]::new();$deviceList.Location=[Drawing.Point]::new(24,58);$deviceList.Size=[Drawing.Size]::new(720,340);$deviceList.CheckOnClick=$true;$devices.Controls.Add($deviceList)
@@ -51,10 +58,10 @@ $cleanupSchedule=Add-WindCheckBox $maintenance 'Install daily Temp and expired m
$lock=Add-WindCheckBox $maintenance 'Install session-lock clipboard clearing' 24 148 $false 650 $lock=Add-WindCheckBox $maintenance 'Install session-lock clipboard clearing' 24 148 $false 650
$vc=[Windows.Forms.TextBox]::new();$vc.Location=[Drawing.Point]::new(48,190);$vc.Width=210;$maintenance.Controls.Add($vc);Add-WindLabel $maintenance 'VeraCrypt drive letters, e.g. X:,Y:' 275 192 330 28|Out-Null $vc=[Windows.Forms.TextBox]::new();$vc.Location=[Drawing.Point]::new(48,190);$vc.Width=210;$maintenance.Controls.Add($vc);Add-WindLabel $maintenance 'VeraCrypt drive letters, e.g. X:,Y:' 275 192 330 28|Out-Null
Add-WindLabel $maintenance 'Optional selected VeraCrypt volumes are unmounted gracefully on lock. Privacy cleanup is ordinary deletion, not guaranteed SSD erasure. Security, Defender, update, firewall and authentication event logs remain protected.' 24 236 700 72|Out-Null Add-WindLabel $maintenance 'Optional selected VeraCrypt volumes are unmounted gracefully on lock. Privacy cleanup is ordinary deletion, not guaranteed SSD erasure. Security, Defender, update, firewall and authentication event logs remain protected.' 24 236 700 72|Out-Null
$securityAudit=[Windows.Forms.Button]::new();$securityAudit.Text='Audit encryption and security';$securityAudit.Location=[Drawing.Point]::new(24,322);$securityAudit.Size=[Drawing.Size]::new(260,38);$maintenance.Controls.Add($securityAudit);$securityAudit.Add_Click({try{Invoke-WindChild 'Security.ps1' @{};[Windows.Forms.MessageBox]::Show('Security status was written to the PowerShell host. No recovery keys were read.','ms-private')|Out-Null}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}}) $securityAudit=[Windows.Forms.Button]::new();$securityAudit.Text='Audit encryption and security';$securityAudit.Location=[Drawing.Point]::new(24,322);$securityAudit.Size=[Drawing.Size]::new(260,38);$maintenance.Controls.Add($securityAudit);$securityAudit.Add_Click({try{$audit=& $invokeWindChild 'Security.ps1' @{Json=$true} -PassThru;Show-WindGuiReview -Text ($audit-join[Environment]::NewLine) -Title 'ms-private security audit'}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}})
Add-WindLabel $homeTab 'Windows Home does not enforce the same policy CSPs as Pro/Enterprise. These buttons open Microsoft-supported Settings pages; verify each visible toggle before treating it as applied.' 20 18 720 70|Out-Null Add-WindLabel $homeTab 'Windows Home does not enforce the same policy CSPs as Pro/Enterprise. These buttons open Microsoft-supported Settings pages; verify each visible toggle before treating it as applied.' 20 18 720 70|Out-Null
$pages=@('Location','Camera','Microphone','Diagnostics','Recall','Updates','Apps','DefaultApps');for($i=0;$i-lt$pages.Count;$i++){$p=$pages[$i];$b=[Windows.Forms.Button]::new();$b.Text=$p;$b.Size=[Drawing.Size]::new(165,38);$b.Location=[Drawing.Point]::new(24+(($i%4)*182),105+([Math]::Floor($i/4)*55));$b.Tag=$p;$b.Add_Click({param($s,$e)Start-Process (Join-Path $PSScriptRoot 'Home.ps1') -ArgumentList @('-Page',[string]$s.Tag)});$homeTab.Controls.Add($b)} $pages=@('Location','Camera','Microphone','Diagnostics','Recall','Updates','Apps','DefaultApps');for($i=0;$i-lt$pages.Count;$i++){$p=$pages[$i];$b=[Windows.Forms.Button]::new();$b.Text=$p;$b.Size=[Drawing.Size]::new(165,38);$b.Location=[Drawing.Point]::new(24+(($i%4)*182),105+([Math]::Floor($i/4)*55));$b.Tag=$p;$b.Add_Click({param($s,$e)try{& $invokeWindChild 'Home.ps1' @{Page=[string]$s.Tag}}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}});$homeTab.Controls.Add($b)}
function Get-WindGuiAppPlan { function Get-WindGuiAppPlan {
$inventory=@(Get-WindAppInventory) $inventory=@(Get-WindAppInventory)
@@ -75,7 +82,9 @@ function Set-WindGuiPreset {
function Get-WindGuiReviewText { function Get-WindGuiReviewText {
param([object[]]$AppPlan,[object[]]$PolicyPlan=@()) param([object[]]$AppPlan,[object[]]$PolicyPlan=@())
$remove=@($AppPlan|Where-Object {$_.Status -eq 'Remove'}|Sort-Object Name) $remove=@($AppPlan|Where-Object {$_.Status -eq 'Remove'}|Sort-Object Name)
$lines=@("Preset: $($preset.SelectedItem)","Privacy: $($profile.SelectedItem); updates: $($updates.SelectedItem)","Privacy services: $($services.Checked); account purge: $($accounts.Checked)","Network: $($netProfile.SelectedItem); Recall deletion/removal: $($recall.Checked)/$($removeRecall.Checked)","Cleanup now/privacy/schedule: $($cleanup.Checked)/$($privacyCleanup.Checked)/$($cleanupSchedule.Checked)","Sensors selected: $($deviceList.CheckedIndices.Count)","Applications selected: $($remove.Count)",'') $supportedCount=@($PolicyPlan|Where-Object {$_.Status-in@('WouldChange','Configured')}).Count;$unsupportedCount=@($PolicyPlan|Where-Object {$_.Status-eq'Unsupported'}).Count
$lines=@("Windows: $($windowsContext.Edition), build $($windowsContext.Build).$($windowsContext.Revision)","Policy controls available: $supportedCount; unsupported on this edition/build: $unsupportedCount","Preset: $($preset.SelectedItem)","Privacy: $($profile.SelectedItem); updates: $($updates.SelectedItem)","Privacy services: $($services.Checked); account purge: $($accounts.Checked)","Network: $($netProfile.SelectedItem); Recall deletion/removal: $($recall.Checked)/$($removeRecall.Checked)","Cleanup now/privacy/schedule: $($cleanup.Checked)/$($privacyCleanup.Checked)/$($cleanupSchedule.Checked)","Sensors selected: $($deviceList.CheckedIndices.Count)","Applications selected: $($remove.Count)",'')
if($windowsContext.Edition-match'^Core' -and$unsupportedCount){$lines+='Windows Home does not support the managed-edition CSP policies below. Direct registry controls explicitly documented for Windows remain available; use the Home guide for the rest.';$lines+=''}
foreach($p in $PolicyPlan){$lines+="POLICY [$($p.Status)] $($p.Id)=$($p.Desired)"} foreach($p in $PolicyPlan){$lines+="POLICY [$($p.Status)] $($p.Id)=$($p.Desired)"}
if($recall.Checked){$rp=Get-WindRecallPlan -RemoveFeature:$removeRecall.Checked;foreach($item in $rp.Items){$lines+="RECALL [$($item.Status)] $($item.Action)"}} if($recall.Checked){$rp=Get-WindRecallPlan -RemoveFeature:$removeRecall.Checked;foreach($item in $rp.Items){$lines+="RECALL [$($item.Status)] $($item.Action)"}}
if($PolicyPlan.Count){$lines+=''} if($PolicyPlan.Count){$lines+=''}
@@ -84,10 +93,10 @@ function Get-WindGuiReviewText {
$lines -join [Environment]::NewLine $lines -join [Environment]::NewLine
} }
function Show-WindGuiReview { function Show-WindGuiReview {
param([string]$Text) param([string]$Text,[string]$Title='ms-private exact plan')
$dialog=[Windows.Forms.Form]::new();$dialog.Text='ms-private exact plan';$dialog.Size=[Drawing.Size]::new(820,620);$dialog.StartPosition='CenterParent';$dialog.Font=$form.Font $dialog=[Windows.Forms.Form]::new();$dialog.Text=$Title;$dialog.Size=[Drawing.Size]::new(820,620);$dialog.StartPosition='CenterParent';$dialog.Font=$form.Font
$box=[Windows.Forms.TextBox]::new();$box.Multiline=$true;$box.ReadOnly=$true;$box.ScrollBars='Both';$box.WordWrap=$false;$box.Dock='Fill';$box.Text=$Text;$dialog.Controls.Add($box) $box=[Windows.Forms.TextBox]::new();$box.Multiline=$true;$box.ReadOnly=$true;$box.ScrollBars='Both';$box.WordWrap=$false;$box.Dock='Fill';$box.Text=$Text;$dialog.Controls.Add($box)
[void]$dialog.ShowDialog($form) try{[void]$dialog.ShowDialog($form)}finally{$dialog.Dispose()}
} }
$script:applyingPreset=$false $script:applyingPreset=$false
$markCustom={if(-not$script:applyingPreset-and[string]$preset.SelectedItem-ne'Custom'){$preset.SelectedItem='Custom'}} $markCustom={if(-not$script:applyingPreset-and[string]$preset.SelectedItem-ne'Custom'){$preset.SelectedItem='Custom'}}
@@ -97,7 +106,7 @@ $rollback.Add_ValueChanged($markCustom);$deviceList.Add_ItemCheck($markCustom)
$preset.Add_SelectedIndexChanged({$selected=@($presetCatalog.Presets|Where-Object Label -CEQ ([string]$preset.SelectedItem));if($selected.Count-eq1){Set-WindGuiPreset $selected[0]}}) $preset.Add_SelectedIndexChanged({$selected=@($presetCatalog.Presets|Where-Object Label -CEQ ([string]$preset.SelectedItem));if($selected.Count-eq1){Set-WindGuiPreset $selected[0]}})
$preset.SelectedItem=(@($presetCatalog.Presets|Where-Object {$_.Id -eq $installContext.RecommendedPreset})[0].Label) $preset.SelectedItem=(@($presetCatalog.Presets|Where-Object {$_.Id -eq $installContext.RecommendedPreset})[0].Label)
$status=Add-WindLabel $form ("Ready. $($installContext.RecommendedPreset) was selected from installation age; review before applying.") 24 580 610 55;$status.ForeColor=[Drawing.Color]::FromArgb(60,70,80) $statusLabel=Add-WindLabel $form ("Ready on $($windowsContext.Edition) build $($windowsContext.Build). $($installContext.RecommendedPreset) was selected from installation age; review before applying.") 24 580 610 55;$statusLabel.ForeColor=[Drawing.Color]::FromArgb(60,70,80)
$review=[Windows.Forms.Button]::new();$review.Text='Review plan';$review.Location=[Drawing.Point]::new(620,575);$review.Size=[Drawing.Size]::new(95,42);$form.Controls.Add($review) $review=[Windows.Forms.Button]::new();$review.Text='Review plan';$review.Location=[Drawing.Point]::new(620,575);$review.Size=[Drawing.Size]::new(95,42);$form.Controls.Add($review)
$apply=[Windows.Forms.Button]::new();$apply.Text='Apply';$apply.Location=[Drawing.Point]::new(722,575);$apply.Size=[Drawing.Size]::new(95,42);$apply.BackColor=[Drawing.Color]::FromArgb(35,105,165);$apply.ForeColor=[Drawing.Color]::White;$form.Controls.Add($apply) $apply=[Windows.Forms.Button]::new();$apply.Text='Apply';$apply.Location=[Drawing.Point]::new(722,575);$apply.Size=[Drawing.Size]::new(95,42);$apply.BackColor=[Drawing.Color]::FromArgb(35,105,165);$apply.ForeColor=[Drawing.Color]::White;$form.Controls.Add($apply)
@@ -116,20 +125,24 @@ $apply.Add_Click({
$answer=[Windows.Forms.MessageBox]::Show("Apply this complete plan now?`r`n`r`n$removeCount applications, $($deviceList.CheckedIndices.Count) devices, account purge=$($accounts.Checked), Recall deletion=$($recall.Checked), network=$($netProfile.SelectedItem).`r`n`r`nApp/account/Recall/cache deletion is not restored by policy rollback. Maximum isolation keeps a timed recovery until separately confirmed.",'Final ms-private approval',[Windows.Forms.MessageBoxButtons]::YesNo,[Windows.Forms.MessageBoxIcon]::Warning);if($answer-ne[Windows.Forms.DialogResult]::Yes){throw 'The complete plan was not approved.'} $answer=[Windows.Forms.MessageBox]::Show("Apply this complete plan now?`r`n`r`n$removeCount applications, $($deviceList.CheckedIndices.Count) devices, account purge=$($accounts.Checked), Recall deletion=$($recall.Checked), network=$($netProfile.SelectedItem).`r`n`r`nApp/account/Recall/cache deletion is not restored by policy rollback. Maximum isolation keeps a timed recovery until separately confirmed.",'Final ms-private approval',[Windows.Forms.MessageBoxButtons]::YesNo,[Windows.Forms.MessageBoxIcon]::Warning);if($answer-ne[Windows.Forms.DialogResult]::Yes){throw 'The complete plan was not approved.'}
$tools=@();if($mullvad.Checked){$tools+='Mullvad'};if($veracrypt.Checked){$tools+='VeraCrypt'};$b=@();if($zen.Checked){$b+='Zen'};if($helium.Checked){$b+='Helium'};if($tor.Checked){$b+='Tor'};$selected=@();foreach($i in $deviceList.CheckedIndices){$selected+=@($sensorItems[[int]$i].InstanceId)} $tools=@();if($mullvad.Checked){$tools+='Mullvad'};if($veracrypt.Checked){$tools+='VeraCrypt'};$b=@();if($zen.Checked){$b+='Zen'};if($helium.Checked){$b+='Helium'};if($tor.Checked){$b+='Tor'};$selected=@();foreach($i in $deviceList.CheckedIndices){$selected+=@($sensorItems[[int]$i].InstanceId)}
$runState=[pscustomobject]@{NetworkJournal=$null};$groups=[Collections.Generic.List[object]]::new() $runState=[pscustomobject]@{NetworkJournal=$null};$groups=[Collections.Generic.List[object]]::new()
if($policyIds.Count-or$policyIssues.Count){$groups.Add([pscustomobject]@{Id='Policies';Label='Registry policy writes';DependsOn=@();Action=({if($policyIds.Count){Invoke-WindChild 'ms-private.ps1' @{Mode='Apply';Profile=[string]$profile.SelectedItem;Updates=[string]$updates.SelectedItem;Id=$policyIds;Confirm=$false}};if($policyIssues.Count){throw "$($policyIssues.Count) requested policies had a conflict or read error; eligible policies were attempted."}}.GetNewClosure())})} if($policyIds.Count-or$policyIssues.Count){$groups.Add([pscustomobject]@{Id='Policies';Label='Registry policy writes';DependsOn=@();Action=({if($policyIds.Count){& $invokeWindChild 'ms-private.ps1' @{Mode='Apply';Profile=[string]$profile.SelectedItem;Updates=[string]$updates.SelectedItem;Id=$policyIds;Confirm=$false}};if($policyIssues.Count){throw "$($policyIssues.Count) requested policies had a conflict or read error; eligible policies were attempted."}}.GetNewClosure())})}
if($services.Checked){$groups.Add([pscustomobject]@{Id='Services';Label='Service configuration';DependsOn=@();Action=({Invoke-WindChild 'Services.ps1' @{Mode='Apply';Profile='Strict';Confirm=$false}}.GetNewClosure())})} if($services.Checked){$groups.Add([pscustomobject]@{Id='Services';Label='Service configuration';DependsOn=@();Action=({& $invokeWindChild 'Services.ps1' @{Mode='Apply';Profile='Strict';Confirm=$false}}.GetNewClosure())})}
if($accounts.Checked){$groups.Add([pscustomobject]@{Id='Accounts';Label='Credential and application-account purge';DependsOn=@();Action=({Invoke-WindChild 'Accounts.ps1' @{Mode='Apply';Scope='AllAppAccounts';AcknowledgeAccountDataDeletion=$true;Confirm=$false}}.GetNewClosure())})} if($accounts.Checked){$groups.Add([pscustomobject]@{Id='Accounts';Label='Credential and application-account purge';DependsOn=@();Action=({& $invokeWindChild 'Accounts.ps1' @{Mode='Apply';Scope='AllAppAccounts';AcknowledgeAccountDataDeletion=$true;Confirm=$false}}.GetNewClosure())})}
if($tools.Count){$groups.Add([pscustomobject]@{Id='PrivacyTools';Label='Privacy-tool installation';DependsOn=@();Action=({Invoke-WindChild 'Install-Apps.ps1' @{App=$tools;Confirm=$false}}.GetNewClosure())})} if($tools.Count){$groups.Add([pscustomobject]@{Id='PrivacyTools';Label='Privacy-tool installation';DependsOn=@();Action=({& $invokeWindChild 'Install-Apps.ps1' @{App=$tools;Confirm=$false}}.GetNewClosure())})}
if($b.Count-or@($appPlan | Where-Object { $_.Status -eq 'Remove' }).Count-or$appIssues.Count){$groups.Add([pscustomobject]@{Id='Applications';Label='Browser installation and exact app removal';DependsOn=@();Action=({if($appIssues.Count){throw "$($appIssues.Count) application targets were blocked or ambiguous; no app removal was attempted."};$a=@{Mode='Apply';Browser=$b;SkipBundled=(-not$bundled.Checked);KeepOneDrive=(-not$oneDrive.Checked);KeepEdge=(-not$edge.Checked);RemovePersonal=$personal.Checked;RemoveStore=$store.Checked;ReviewedPlan=$appPlan;Confirm=$false};Invoke-WindChild 'Manage-Apps.ps1' $a}.GetNewClosure())})} if($b.Count-or@($appPlan | Where-Object { $_.Status -eq 'Remove' }).Count-or$appIssues.Count){$groups.Add([pscustomobject]@{Id='Applications';Label='Browser installation and exact app removal';DependsOn=@();Action=({if($appIssues.Count){throw "$($appIssues.Count) application targets were blocked or ambiguous; no app removal was attempted."};$a=@{Mode='Apply';Browser=$b;SkipBundled=(-not$bundled.Checked);KeepOneDrive=(-not$oneDrive.Checked);KeepEdge=(-not$edge.Checked);RemovePersonal=$personal.Checked;RemoveStore=$store.Checked;ReviewedPlan=$appPlan;Confirm=$false};& $invokeWindChild 'Manage-Apps.ps1' $a}.GetNewClosure())})}
if($netProfile.SelectedItem-ne'Observe'){$groups.Add([pscustomobject]@{Id='Network';Label='Firewall changes';DependsOn=@();Action=({$networkRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\network';[IO.Directory]::CreateDirectory($networkRoot)|Out-Null;$runState.NetworkJournal=Join-Path $networkRoot ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');Invoke-WindChild 'Network.ps1' @{Mode='Apply';Profile=[string]$netProfile.SelectedItem;RollbackMinutes=[int]$rollback.Value;Journal=$runState.NetworkJournal;Confirm=$false}}.GetNewClosure())})} if($netProfile.SelectedItem-ne'Observe'){$groups.Add([pscustomobject]@{Id='Network';Label='Firewall changes';DependsOn=@();Action=({$networkRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\network';[IO.Directory]::CreateDirectory($networkRoot)|Out-Null;$runState.NetworkJournal=Join-Path $networkRoot ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');& $invokeWindChild 'Network.ps1' @{Mode='Apply';Profile=[string]$netProfile.SelectedItem;RollbackMinutes=[int]$rollback.Value;Journal=$runState.NetworkJournal;Confirm=$false}}.GetNewClosure())})}
if($selected.Count){$groups.Add([pscustomobject]@{Id='Sensors';Label='Exact device disablement';DependsOn=@();Action=({Invoke-WindChild 'Hardware.ps1' @{Mode='Apply';InstanceId=$selected;Confirm=$false}}.GetNewClosure())})} if($selected.Count){$groups.Add([pscustomobject]@{Id='Sensors';Label='Exact device disablement';DependsOn=@();Action=({& $invokeWindChild 'Hardware.ps1' @{Mode='Apply';InstanceId=$selected;Confirm=$false}}.GetNewClosure())})}
if($recall.Checked-and($recallDisposition.Applicable-or$recallDisposition.Conflict)){$groups.Add([pscustomobject]@{Id='Recall';Label='Recall policy, snapshot and feature removal';DependsOn=@();Action=({if($recallDisposition.Conflict){throw 'The Recall plan contains a conflicting value.'};Invoke-WindChild 'Recall.ps1' @{Mode='Apply';AcknowledgeSnapshotDeletion=$true;RemoveFeature=[bool]$removeRecall.Checked;Confirm=$false}}.GetNewClosure())})} if($recall.Checked-and($recallDisposition.Applicable-or$recallDisposition.Conflict)){$groups.Add([pscustomobject]@{Id='Recall';Label='Recall policy, snapshot and feature removal';DependsOn=@();Action=({if($recallDisposition.Conflict){throw 'The Recall plan contains a conflicting value.'};& $invokeWindChild 'Recall.ps1' @{Mode='Apply';AcknowledgeSnapshotDeletion=$true;RemoveFeature=[bool]$removeRecall.Checked;Confirm=$false}}.GetNewClosure())})}
if($cleanup.Checked-or$privacyCleanup.Checked){$groups.Add([pscustomobject]@{Id='Cleanup';Label='Allowlisted file cleanup';DependsOn=@();Action=({if($cleanup.Checked){Invoke-WindChild 'Cleanup.ps1' @{Mode='Run';AgeDays=7;Category=@('Temp');Confirm=$false}};if($privacyCleanup.Checked){Invoke-WindChild 'Cleanup.ps1' @{Mode='Run';AgeDays=0;Category=@('CrashReports','RecentItems','ThumbnailCache','BrowserCache','PowerShellHistory');ClearClipboard=$true;Confirm=$false}}}.GetNewClosure())})} if($cleanup.Checked-or$privacyCleanup.Checked){$groups.Add([pscustomobject]@{Id='Cleanup';Label='Allowlisted file cleanup';DependsOn=@();Action=({if($cleanup.Checked){& $invokeWindChild 'Cleanup.ps1' @{Mode='Run';AgeDays=7;Category=@('Temp');Confirm=$false}};if($privacyCleanup.Checked){& $invokeWindChild 'Cleanup.ps1' @{Mode='Run';AgeDays=0;Category=@('CrashReports','RecentItems','ThumbnailCache','BrowserCache','PowerShellHistory');ClearClipboard=$true;Confirm=$false}}}.GetNewClosure())})}
if($cleanupSchedule.Checked){$groups.Add([pscustomobject]@{Id='CleanupSchedule';Label='Cleanup schedule';DependsOn=@();Action=({Invoke-WindChild 'Cleanup.ps1' @{Mode='InstallSchedule';AgeDays=7;Confirm=$false}}.GetNewClosure())})} if($cleanupSchedule.Checked){$groups.Add([pscustomobject]@{Id='CleanupSchedule';Label='Cleanup schedule';DependsOn=@();Action=({& $invokeWindChild 'Cleanup.ps1' @{Mode='InstallSchedule';AgeDays=7;Confirm=$false}}.GetNewClosure())})}
if($lock.Checked){$vols=@($vc.Text.Split(',')|ForEach-Object{$_.Trim().ToUpperInvariant()}|Where-Object{$_});$groups.Add([pscustomobject]@{Id='LockHandler';Label='Session-lock handler';DependsOn=@();Action=({Invoke-WindChild 'Lock.ps1' @{Mode='Install';VeraCryptVolume=$vols;Confirm=$false}}.GetNewClosure())})} if($lock.Checked){$vols=@($vc.Text.Split(',')|ForEach-Object{$_.Trim().ToUpperInvariant()}|Where-Object{$_});$groups.Add([pscustomobject]@{Id='LockHandler';Label='Session-lock handler';DependsOn=@();Action=({& $invokeWindChild 'Lock.ps1' @{Mode='Install';VeraCryptVolume=$vols;Confirm=$false}}.GetNewClosure())})}
$runRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\runs';$runPath=Join-Path $runRoot ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');$progress={param($entry)$status.Text='Running: '+$entry.Label;$form.Refresh()}.GetNewClosure();$runResult=Invoke-MsPrivateGroupPlan -Groups @($groups) -OnProgress $progress -ReportPath $runPath $runRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\runs';$runPath=Join-Path $runRoot ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');$statusLabel.Text='Running the approved plan...';$form.Refresh();$runResult=Invoke-MsPrivateGroupPlan -Groups @($groups) -ReportPath $runPath
$networkEntry=@($runResult.Entries | Where-Object { $_.Id -eq 'Network' });if($netProfile.SelectedItem-eq'Isolate'-and$networkEntry.Count-and$networkEntry[0].Status-eq'Succeeded'){$answer=[Windows.Forms.MessageBox]::Show("Isolation is active. Verify required connectivity and local recovery now.`r`n`r`nYes keeps the block profile and cancels timed rollback.`r`nNo restores immediately. Closing this prompt leaves the timed rollback active.`r`n`r`nJournal: $($runState.NetworkJournal)",'Confirm network isolation',[Windows.Forms.MessageBoxButtons]::YesNoCancel,[Windows.Forms.MessageBoxIcon]::Warning);if($answer-eq[Windows.Forms.DialogResult]::Yes){Invoke-WindChild 'Network.ps1' @{Mode='Confirm';Journal=$runState.NetworkJournal}}elseif($answer-eq[Windows.Forms.DialogResult]::No){Invoke-WindChild 'Network.ps1' @{Mode='Restore';Journal=$runState.NetworkJournal}}} $networkEntry=@($runResult.Entries | Where-Object { $_.Id -eq 'Network' });if($netProfile.SelectedItem-eq'Isolate'-and$networkEntry.Count-and$networkEntry[0].Status-eq'Succeeded'){$answer=[Windows.Forms.MessageBox]::Show("Isolation is active. Verify required connectivity and local recovery now.`r`n`r`nYes keeps the block profile and cancels timed rollback.`r`nNo restores immediately. Closing this prompt leaves the timed rollback active.`r`n`r`nJournal: $($runState.NetworkJournal)",'Confirm network isolation',[Windows.Forms.MessageBoxButtons]::YesNoCancel,[Windows.Forms.MessageBoxIcon]::Warning);if($answer-eq[Windows.Forms.DialogResult]::Yes){& $invokeWindChild 'Network.ps1' @{Mode='Confirm';Journal=$runState.NetworkJournal}}elseif($answer-eq[Windows.Forms.DialogResult]::No){& $invokeWindChild 'Network.ps1' @{Mode='Restore';Journal=$runState.NetworkJournal}}}
$failed=@($runResult.Entries | Where-Object { $_.Status -in @('Failed','SkippedDependency') });$status.Text=if($failed.Count){"Finished with $($failed.Count) failed/skipped groups. Independent groups were attempted. Report: $runPath"}elseif($recallDisposition.Skipped.Count){'Completed with unsupported Recall actions skipped. Review each report and use the Home guide.'}else{'Completed. Review each generated journal/report and reboot before validation.'};[Windows.Forms.MessageBox]::Show($status.Text,'ms-private', [Windows.Forms.MessageBoxButtons]::OK,$(if($failed.Count){[Windows.Forms.MessageBoxIcon]::Warning}else{[Windows.Forms.MessageBoxIcon]::Information}))|Out-Null $failed=@($runResult.Entries | Where-Object { $_.Status -in @('Failed','SkippedDependency') })
}catch{$status.Text='Stopped: '+$_.Exception.Message;[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error',[Windows.Forms.MessageBoxButtons]::OK,[Windows.Forms.MessageBoxIcon]::Error)|Out-Null} $statusLabel.Text=if($failed.Count){"Finished with $($failed.Count) failed/skipped groups. Report: $runPath"}elseif($recallDisposition.Skipped.Count){'Completed with unsupported Recall actions skipped. Review each report and use the Home guide.'}else{'Completed. Review each generated journal/report and reboot before validation.'}
$message=$statusLabel.Text
if($failed.Count){$details=@($failed|ForEach-Object{"$($_.Label): $($_.Error)"});$message+="`r`n`r`n"+($details-join"`r`n")}
[Windows.Forms.MessageBox]::Show($message,'ms-private',[Windows.Forms.MessageBoxButtons]::OK,$(if($failed.Count){[Windows.Forms.MessageBoxIcon]::Warning}else{[Windows.Forms.MessageBoxIcon]::Information}))|Out-Null
}catch{$statusLabel.Text='Stopped: '+$_.Exception.Message;[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error',[Windows.Forms.MessageBoxButtons]::OK,[Windows.Forms.MessageBoxIcon]::Error)|Out-Null}
}) })
[void]$form.ShowDialog() [void]$form.ShowDialog()
@@ -19,9 +19,9 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>ms-private</AssemblyName> <AssemblyName>ms-private</AssemblyName>
<RootNamespace>MsPrivate.Launcher</RootNamespace> <RootNamespace>MsPrivate.Launcher</RootNamespace>
<Version>0.0.1</Version> <Version>0.0.6</Version>
<AssemblyVersion>0.0.1.0</AssemblyVersion> <AssemblyVersion>0.0.6.0</AssemblyVersion>
<FileVersion>0.0.1.0</FileVersion> <FileVersion>0.0.6.0</FileVersion>
<Product>ms-private</Product> <Product>ms-private</Product>
<Description>Windows privacy control center</Description> <Description>Windows privacy control center</Description>
</PropertyGroup> </PropertyGroup>
+21 -8
View File
@@ -2,9 +2,22 @@ Set-StrictMode -Version Latest
$ErrorActionPreference='Stop' $ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function New-MsPrivateScriptInvoker {
param([Parameter(Mandatory)][string]$Root)
$runtimeRoot=[IO.Path]::GetFullPath($Root).TrimEnd([IO.Path]::DirectorySeparatorChar)+[IO.Path]::DirectorySeparatorChar
if(-not[IO.Directory]::Exists($runtimeRoot)){throw "Runtime root does not exist: $runtimeRoot"}
{
param([Parameter(Mandatory)][string]$Script,[hashtable]$Parameters=@{},[switch]$PassThru)
if($Script-notmatch'^[A-Za-z0-9][A-Za-z0-9.-]*\.ps1$'){throw "Invalid runtime script name: $Script"}
$path=[IO.Path]::GetFullPath((Join-Path $runtimeRoot $Script))
if(-not$path.StartsWith($runtimeRoot,[StringComparison]::OrdinalIgnoreCase)-or-not[IO.File]::Exists($path)){throw "Runtime script was not found: $Script"}
if($PassThru){& $path @Parameters}else{& $path @Parameters|Out-Host}
}.GetNewClosure()
}
function Invoke-MsPrivateGroupPlan { function Invoke-MsPrivateGroupPlan {
param( param(
[Parameter(Mandatory)][object[]]$Groups, [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Groups,
[scriptblock]$OnProgress, [scriptblock]$OnProgress,
[string]$ReportPath [string]$ReportPath
) )
@@ -17,23 +30,23 @@ function Invoke-MsPrivateGroupPlan {
} }
$entries=@($Groups|ForEach-Object{[pscustomobject]@{Id=[string]$_.Id;Label=[string]$_.Label;DependsOn=@($_.DependsOn);Status='Pending';Error=$null}}) $entries=@($Groups|ForEach-Object{[pscustomobject]@{Id=[string]$_.Id;Label=[string]$_.Label;DependsOn=@($_.DependsOn);Status='Pending';Error=$null}})
$report=[pscustomobject]@{Schema=1;Kind='MsPrivateRun';CreatedUtc=[DateTime]::UtcNow.ToString('o');CompletedUtc=$null;Status='Running';Entries=$entries} $report=[pscustomobject]@{Schema=1;Kind='MsPrivateRun';CreatedUtc=[DateTime]::UtcNow.ToString('o');CompletedUtc=$null;Status='Running';Entries=$entries}
$save={if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8}}.GetNewClosure();&$save if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8}
foreach($group in $Groups){ foreach($group in $Groups){
$entry=@($entries | Where-Object { $_.Id -ceq [string]$group.Id })[0] $entry=@($entries | Where-Object { $_.Id -ceq [string]$group.Id })[0]
$blocked=@($entry.DependsOn | Where-Object { $blocked=@($entry.DependsOn | Where-Object {
$dependency=[string]$_ $dependency=[string]$_
$dependencyEntry=@($entries | Where-Object { $_.Id -ceq $dependency }) $dependencyEntry=@($entries | Where-Object { $_.Id -eq $dependency })
$dependencyEntry.Count -ne 1 -or $dependencyEntry[0].Status -ne 'Succeeded' $dependencyEntry.Count -ne 1 -or $dependencyEntry[0].Status -ne 'Succeeded'
}) })
if($blocked.Count){$entry.Status='SkippedDependency';$entry.Error='Required group did not succeed: '+($blocked-join', ');&$save;continue} if($blocked.Count){$entry.Status='SkippedDependency';$entry.Error='Required group did not succeed: '+($blocked-join', ');if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8};continue}
$entry.Status='Running';&$save;if($OnProgress){&$OnProgress $entry} $entry.Status='Running';if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8};if($OnProgress){&$OnProgress $entry}
try{& $group.Action|Out-Host;$entry.Status='Succeeded'}catch{$entry.Status='Failed';$entry.Error=$_.Exception.Message} try{& $group.Action|Out-Host;$entry.Status='Succeeded'}catch{$entry.Status='Failed';$entry.Error=$_.Exception.Message}
&$save if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8}
} }
$failed=@($entries | Where-Object { $_.Status -eq 'Failed' }).Count $failed=@($entries | Where-Object { $_.Status -eq 'Failed' }).Count
$skipped=@($entries | Where-Object { $_.Status -eq 'SkippedDependency' }).Count $skipped=@($entries | Where-Object { $_.Status -eq 'SkippedDependency' }).Count
$report.Status=if($failed-or$skipped){'PartialFailure'}else{'Completed'};$report.CompletedUtc=[DateTime]::UtcNow.ToString('o');&$save $report.Status=if($failed-or$skipped){'PartialFailure'}else{'Completed'};$report.CompletedUtc=[DateTime]::UtcNow.ToString('o');if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8}
$report $report
} }
Export-ModuleMember -Function Invoke-MsPrivateGroupPlan Export-ModuleMember -Function Invoke-MsPrivateGroupPlan,New-MsPrivateScriptInvoker
+12 -1
View File
@@ -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
+10 -6
View File
@@ -26,6 +26,7 @@ function Get-WindBrowserCacheCatalog {
} }
function Get-WindPrivacyCleanupCatalog { function Get-WindPrivacyCleanupCatalog {
param([bool]$IncludeBrowserCache=$true)
$local=[Environment]::GetFolderPath('LocalApplicationData');$roaming=[Environment]::GetFolderPath('ApplicationData');$program=[Environment]::GetFolderPath('CommonApplicationData') $local=[Environment]::GetFolderPath('LocalApplicationData');$roaming=[Environment]::GetFolderPath('ApplicationData');$program=[Environment]::GetFolderPath('CommonApplicationData')
foreach($root in Get-WindCleanupRoots){[pscustomobject]@{Category='Temp';Root=$root;Patterns=@('*');Recurse=$true;Scheduled=$true;Description='Old files in allowlisted Windows temporary directories'}} foreach($root in Get-WindCleanupRoots){[pscustomobject]@{Category='Temp';Root=$root;Patterns=@('*');Recurse=$true;Scheduled=$true;Description='Old files in allowlisted Windows temporary directories'}}
foreach($root in @((Join-Path $local 'Microsoft\Windows\WER'),(Join-Path $program 'Microsoft\Windows\WER\ReportArchive'),(Join-Path $program 'Microsoft\Windows\WER\ReportQueue'),(Join-Path $program 'Microsoft\Windows\WER\Temp'))){[pscustomobject]@{Category='CrashReports';Root=$root;Patterns=@('*');Recurse=$true;Scheduled=$false;Description='Queued and archived Windows Error Reporting data, which can include dumps and user content'}} foreach($root in @((Join-Path $local 'Microsoft\Windows\WER'),(Join-Path $program 'Microsoft\Windows\WER\ReportArchive'),(Join-Path $program 'Microsoft\Windows\WER\ReportQueue'),(Join-Path $program 'Microsoft\Windows\WER\Temp'))){[pscustomobject]@{Category='CrashReports';Root=$root;Patterns=@('*');Recurse=$true;Scheduled=$false;Description='Queued and archived Windows Error Reporting data, which can include dumps and user content'}}
@@ -33,7 +34,7 @@ function Get-WindPrivacyCleanupCatalog {
[pscustomobject]@{Category='RecentItems';Root=(Join-Path $roaming 'Microsoft\Windows\Recent');Patterns=@('*.lnk','*.automaticDestinations-ms','*.customDestinations-ms');Recurse=$true;Scheduled=$false;Description='Current-user Recent Items and Jump Lists'} [pscustomobject]@{Category='RecentItems';Root=(Join-Path $roaming 'Microsoft\Windows\Recent');Patterns=@('*.lnk','*.automaticDestinations-ms','*.customDestinations-ms');Recurse=$true;Scheduled=$false;Description='Current-user Recent Items and Jump Lists'}
[pscustomobject]@{Category='ThumbnailCache';Root=(Join-Path $local 'Microsoft\Windows\Explorer');Patterns=@('thumbcache_*.db','iconcache_*.db');Recurse=$false;Scheduled=$false;Description='Current-user thumbnail and icon caches; files in use are skipped'} [pscustomobject]@{Category='ThumbnailCache';Root=(Join-Path $local 'Microsoft\Windows\Explorer');Patterns=@('thumbcache_*.db','iconcache_*.db');Recurse=$false;Scheduled=$false;Description='Current-user thumbnail and icon caches; files in use are skipped'}
[pscustomobject]@{Category='PowerShellHistory';Root=(Join-Path $roaming 'Microsoft\Windows\PowerShell\PSReadLine');Patterns=@('*_history.txt');Recurse=$false;Scheduled=$false;Description='Current-user PowerShell interactive command history'} [pscustomobject]@{Category='PowerShellHistory';Root=(Join-Path $roaming 'Microsoft\Windows\PowerShell\PSReadLine');Patterns=@('*_history.txt');Recurse=$false;Scheduled=$false;Description='Current-user PowerShell interactive command history'}
foreach($target in Get-WindBrowserCacheCatalog){$target} if($IncludeBrowserCache){foreach($target in Get-WindBrowserCacheCatalog){$target}}
foreach($root in @((Join-Path $local 'MsPrivate\apps'),(Join-Path $local 'MsPrivate\cleanup'))){[pscustomobject]@{Category='WindReports';Root=$root;Patterns=@('*.json');Recurse=$true;Scheduled=$true;Description='Expired ms-private operation reports; policy, hardware, Recall and network recovery journals are excluded'}} foreach($root in @((Join-Path $local 'MsPrivate\apps'),(Join-Path $local 'MsPrivate\cleanup'))){[pscustomobject]@{Category='WindReports';Root=$root;Patterns=@('*.json');Recurse=$true;Scheduled=$true;Description='Expired ms-private operation reports; policy, hardware, Recall and network recovery journals are excluded'}}
[pscustomobject]@{Category='WindReports';Root=(Join-Path $local 'MsPrivate\network');Patterns=@('capture.etl','capture.pcapng','connections.json');Recurse=$true;Scheduled=$true;Description='Expired ms-private packet captures and connection snapshots; firewall recovery journals are excluded'} [pscustomobject]@{Category='WindReports';Root=(Join-Path $local 'MsPrivate\network');Patterns=@('capture.etl','capture.pcapng','connections.json');Recurse=$true;Scheduled=$true;Description='Expired ms-private packet captures and connection snapshots; firewall recovery journals are excluded'}
} }
@@ -56,7 +57,7 @@ function Test-WindCleanupPathSafe {
function Get-WindCleanupPlan { function Get-WindCleanupPlan {
param([ValidateRange(0,365)][int]$AgeDays=7,[ValidateSet('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports')][string[]]$Category=@('Temp'),[string[]]$Root,[datetime]$Now=(Get-Date)) param([ValidateRange(0,365)][int]$AgeDays=7,[ValidateSet('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports')][string[]]$Category=@('Temp'),[string[]]$Root,[datetime]$Now=(Get-Date))
$cutoff=$Now.AddDays(-$AgeDays);$catalog=@(Get-WindPrivacyCleanupCatalog|Where-Object{$Category-contains$_.Category}) $cutoff=$Now.AddDays(-$AgeDays);$catalog=@(Get-WindPrivacyCleanupCatalog -IncludeBrowserCache ($Category-contains'BrowserCache')|Where-Object{$Category-contains$_.Category})
if($PSBoundParameters.ContainsKey('Root')){ if($PSBoundParameters.ContainsKey('Root')){
if(@($Category|Select-Object -Unique).Count-ne 1-or$Category[0]-ne'Temp'){throw '-Root is available only for the Temp category.'} if(@($Category|Select-Object -Unique).Count-ne 1-or$Category[0]-ne'Temp'){throw '-Root is available only for the Temp category.'}
$allowed=@(Get-WindCleanupRoots|ForEach-Object{[IO.Path]::GetFullPath($_).TrimEnd('\')});$catalog=@($Root|ForEach-Object{$full=[IO.Path]::GetFullPath($_).TrimEnd('\');if($allowed-notcontains$full){throw "Cleanup root is not allowlisted: $full"};[pscustomobject]@{Category='Temp';Root=$full;Patterns=@('*');Recurse=$true}}) $allowed=@(Get-WindCleanupRoots|ForEach-Object{[IO.Path]::GetFullPath($_).TrimEnd('\')});$catalog=@($Root|ForEach-Object{$full=[IO.Path]::GetFullPath($_).TrimEnd('\');if($allowed-notcontains$full){throw "Cleanup root is not allowlisted: $full"};[pscustomobject]@{Category='Temp';Root=$full;Patterns=@('*');Recurse=$true}})
@@ -86,16 +87,19 @@ function Get-WindCleanupPlan {
function Invoke-WindCleanup { function Invoke-WindCleanup {
[CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)]$Plan,[Parameter(Mandatory)][string]$ReportPath) [CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)]$Plan,[Parameter(Mandatory)][string]$ReportPath)
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.'} 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() $catalog=@(Get-WindPrivacyCleanupCatalog -IncludeBrowserCache ($Plan.Categories-contains'BrowserCache')|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=@()} $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 if(-not$PSCmdlet.ShouldProcess('Selected cleanup files','Delete planned privacy artifacts and write cleanup report')){$report.Status='Preview';return $report}
$save=New-MsPrivateJsonWriter -Path $ReportPath -Depth 7;&$save $report
foreach($entry in @($Plan.Files)){ 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))}) $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 $out=[pscustomobject]@{Category=$entry.Category;Path=$path;Status='Checking';Reason=$null};$report.Entries+=@($out);&$save $report
try{ try{
if($matches.Count-ne 1-or-not(Test-WindCleanupPathSafe -Path $path -Root $root)){throw 'Path escaped an allowlisted category target or crosses a reparse point.'} if($matches.Count-ne 1-or-not(Test-WindCleanupPathSafe -Path $path -Root $root)){throw 'Path escaped an allowlisted category target or crosses a reparse point.'}
$item=Get-Item -LiteralPath $path -Force -ErrorAction Stop;if($item.PSIsContainer-or($item.Attributes-band[IO.FileAttributes]::ReparsePoint)-or$item.LastWriteTimeUtc-ge$cutoff){$out.Status='Skipped';$out.Reason='Changed since plan or not a regular old file.';$report.Skipped++;continue} $item=Get-Item -LiteralPath $path -Force -ErrorAction Stop
if($PSCmdlet.ShouldProcess($path,'Delete selected privacy artifact')){$length=[long]$item.Length;Remove-Item -LiteralPath $path -Force -ErrorAction Stop;$out.Status='Deleted';$report.Deleted++;$report.FreedBytes+=$length}else{$out.Status='Preview'} $plannedWrite=[datetime]::Parse([string]$entry.LastWriteUtc,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::RoundtripKind).ToUniversalTime()
if($item.PSIsContainer-or($item.Attributes-band[IO.FileAttributes]::ReparsePoint)-or$item.LastWriteTimeUtc-ge$cutoff-or$item.LastWriteTimeUtc-ne$plannedWrite-or$item.Length-ne[long]$entry.Length){$out.Status='Skipped';$out.Reason='Changed since plan or not a regular old file.';$report.Skipped++;continue}
$length=[long]$item.Length;Remove-Item -LiteralPath $path -Force -ErrorAction Stop;$out.Status='Deleted';$report.Deleted++;$report.FreedBytes+=$length
}catch{$out.Status='Skipped';$out.Reason=$_.Exception.Message;$report.Skipped++}finally{&$save $report} }catch{$out.Status='Skipped';$out.Reason=$_.Exception.Message;$report.Skipped++}finally{&$save $report}
} }
$report.Status='Completed';&$save $report;$report $report.Status='Completed';&$save $report;$report
+10 -2
View File
@@ -30,6 +30,7 @@ function Get-WindCatalog {
if($registryPath-notmatch'^(SOFTWARE|SYSTEM)\\[A-Za-z0-9 _.-]+(?:\\[A-Za-z0-9 _.-]+)*$'-or$registryPath-match'(^|\\)\.\.?($|\\)'){throw "Invalid policy registry path: $id"} if($registryPath-notmatch'^(SOFTWARE|SYSTEM)\\[A-Za-z0-9 _.-]+(?:\\[A-Za-z0-9 _.-]+)*$'-or$registryPath-match'(^|\\)\.\.?($|\\)'){throw "Invalid policy registry path: $id"}
if($name-notmatch'^[A-Za-z0-9_]+$'-or$targets.ContainsKey($target)){throw "Invalid or duplicate policy registry target: $id"} if($name-notmatch'^[A-Za-z0-9_]+$'-or$targets.ContainsKey($target)){throw "Invalid or duplicate policy registry target: $id"}
foreach($profile in @('Standard','Strict')){$value=$item.$profile;if($null-eq$value){continue};if($value-isnot[ValueType]-or[double]$value-ne[int]$value-or[int]$value-lt0){throw "Invalid $profile policy value: $id"}} foreach($profile in @('Standard','Strict')){$value=$item.$profile;if($null-eq$value){continue};if($value-isnot[ValueType]-or[double]$value-ne[int]$value-or[int]$value-lt0){throw "Invalid $profile policy value: $id"}}
if($item.PSObject.Properties['HomeSupported']-and$item.HomeSupported-isnot[bool]){throw "HomeSupported must be a Boolean: $id"}
if($null-ne$item.EnterpriseValue-and($item.EnterpriseValue-isnot[ValueType]-or[double]$item.EnterpriseValue-ne[int]$item.EnterpriseValue-or[int]$item.EnterpriseValue-lt0)){throw "Invalid Enterprise policy value: $id"} if($null-ne$item.EnterpriseValue-and($item.EnterpriseValue-isnot[ValueType]-or[double]$item.EnterpriseValue-ne[int]$item.EnterpriseValue-or[int]$item.EnterpriseValue-lt0)){throw "Invalid Enterprise policy value: $id"}
if($null-eq$item.Standard-and$null-eq$item.Strict-and$null-eq$item.EnterpriseValue-and$id-notin@('updates-auto','updates-options')){throw "Policy has no selectable value: $id"} if($null-eq$item.Standard-and$null-eq$item.Strict-and$null-eq$item.EnterpriseValue-and$id-notin@('updates-auto','updates-options')){throw "Policy has no selectable value: $id"}
$source=$null;if(-not[uri]::TryCreate([string]$item.Source,[UriKind]::Absolute,[ref]$source)-or$source.Scheme-ne'https'){throw "Policy requires an HTTPS source: $id"} $source=$null;if(-not[uri]::TryCreate([string]$item.Source,[UriKind]::Absolute,[ref]$source)-or$source.Scheme-ne'https'){throw "Policy requires an HTTPS source: $id"}
@@ -116,6 +117,7 @@ function Get-WindPlan {
if ($catalog.Id -notcontains $item) { throw "Unknown policy ID: $item" } if ($catalog.Id -notcontains $item) { throw "Unknown policy ID: $item" }
} }
$supportedEdition = $Context.Edition -in @('Professional','ProfessionalN','ProfessionalEducation','ProfessionalEducationN','ProfessionalWorkstation','ProfessionalWorkstationN','Enterprise','EnterpriseN','Education','EducationN','EnterpriseS','EnterpriseSN') $supportedEdition = $Context.Edition -in @('Professional','ProfessionalN','ProfessionalEducation','ProfessionalEducationN','ProfessionalWorkstation','ProfessionalWorkstationN','Enterprise','EnterpriseN','Education','EducationN','EnterpriseS','EnterpriseSN')
$homeEdition = $Context.Edition -in @('Core','CoreN','CoreSingleLanguage','CoreCountrySpecific')
$supportedBuild = $Context.Build -in @(26100,26200) $supportedBuild = $Context.Build -in @(26100,26200)
foreach ($policy in $catalog) { foreach ($policy in $catalog) {
if ($Id.Count -gt 0 -and $Id -notcontains $policy.Id) { continue } if ($Id.Count -gt 0 -and $Id -notcontains $policy.Id) { continue }
@@ -138,9 +140,15 @@ function Get-WindPlan {
$status = 'Conflict'; $reason = 'Existing per-app exceptions need review; default deny would not cover all apps.' $status = 'Conflict'; $reason = 'Existing per-app exceptions need review; default deny would not cover all apps.'
} }
} }
if (-not $supportedEdition -or -not $supportedBuild) { $homeSupported=$policy.PSObject.Properties['HomeSupported']-and[bool]$policy.HomeSupported
if (-not $supportedBuild) {
$status = 'Unsupported' $status = 'Unsupported'
$reason = 'This edition/build is audit-only. Registry presence does not establish policy enforcement; use Guide.' $reason = 'This Windows build is not reviewed for policy application. Registry presence does not establish enforcement.'
} elseif (-not $supportedEdition -and-not($homeEdition-and$homeSupported)) {
$status = 'Unsupported'
$reason = if($homeEdition){'Microsoft documents this policy for managed Pro/Enterprise/Education editions, not Windows Home. Use the Home guide.'}else{'This Windows edition is not reviewed for policy application.'}
} elseif($homeEdition-and$homeSupported) {
$reason = 'Microsoft documents this direct registry control independently of the managed-edition CSP. Behavioral verification remains required on Home.'
} }
} catch { } catch {
$status = 'ReadError'; $reason = 'Could not read policy state. No protection can be inferred.' $status = 'ReadError'; $reason = 'Could not read policy state. No protection can be inferred.'
+1 -1
View File
@@ -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.'} $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} 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'}})} $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} 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}
} }
+35 -2
View File
@@ -104,7 +104,7 @@ function Invoke-WindNetworkApply {
$backup=$JournalPath+'.firewall.wfw' $backup=$JournalPath+'.firewall.wfw'
if($Profile-eq'Isolate'){&$Backend.ExportPolicy $backup} 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}} $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 & $save $journal
try{ try{
if($Profile -eq 'BlockOptionalTelemetry'){ if($Profile -eq 'BlockOptionalTelemetry'){
@@ -167,4 +167,37 @@ function Get-WindNetworkValidation {
} }
} }
Export-ModuleMember -Function Get-WindNetworkCatalog,Get-WindNetworkCapabilities,Get-WindNetworkPlan,New-WindNetworkBackend,Invoke-WindNetworkApply,Restore-WindNetwork,Confirm-WindNetwork,Get-WindNetworkValidation,Assert-WindNetworkJournal function Resolve-WindCaptureDirectory {
param([Parameter(Mandatory)][string]$Path,[Parameter(Mandatory)][string]$StateRoot)
if($Path-notmatch'^[A-Za-z]:[\\/]'){throw 'Capture directory must be an absolute local path.'}
$root=[IO.Path]::GetFullPath($StateRoot).TrimEnd('\')
$full=[IO.Path]::GetFullPath($Path).TrimEnd('\')
if(-not$full.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase)-or$full.Substring(2).Contains(':')){throw 'Capture directory must be inside the ms-private local state directory.'}
$cursor=$full
while($cursor){
if(Test-Path -LiteralPath $cursor){
$item=Get-Item -LiteralPath $cursor -Force -ErrorAction Stop
if(-not$item.PSIsContainer-or($item.Attributes-band[IO.FileAttributes]::ReparsePoint)){throw 'Capture path crosses a file or reparse point.'}
}
$next=[IO.Path]::GetDirectoryName($cursor);if($next-eq$cursor){break};$cursor=$next
}
$full
}
function Invoke-WindPacketMonitor {
param(
[Parameter(Mandatory)][string[]]$ArgumentList,
[scriptblock]$Runner={
param($NativeArguments)
$executable=Join-Path $env:SystemRoot 'System32\pktmon.exe'
if(-not(Test-Path -LiteralPath $executable -PathType Leaf)){throw 'pktmon.exe is unavailable.'}
& $executable @NativeArguments|Out-Host
$LASTEXITCODE
}
)
$result=@(& $Runner $ArgumentList)
if($result.Count-ne1-or$result[0]-isnot[int]){throw 'Packet monitor did not return a valid exit code.'}
if($result[0]-ne0){throw "pktmon $($ArgumentList[0]) failed with exit code $($result[0])."}
}
Export-ModuleMember -Function Get-WindNetworkCatalog,Get-WindNetworkCapabilities,Get-WindNetworkPlan,New-WindNetworkBackend,Invoke-WindNetworkApply,Restore-WindNetwork,Confirm-WindNetwork,Get-WindNetworkValidation,Assert-WindNetworkJournal,Resolve-WindCaptureDirectory,Invoke-WindPacketMonitor
+1 -1
View File
@@ -40,7 +40,7 @@ function Invoke-WindRecallApply {
if(-not $PSCmdlet.ShouldProcess('Windows Recall','Disable Recall and delete existing snapshots')){return $plan} if(-not $PSCmdlet.ShouldProcess('Windows Recall','Disable Recall and delete existing snapshots')){return $plan}
$s=$plan.State $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.'} $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{ try{
Set-WindRecallPolicyState ([pscustomobject]@{Exists=$true;Value=1}) Set-WindRecallPolicyState ([pscustomobject]@{Exists=$true;Value=1})
$after=Get-WindRecallState;if(-not $after.PolicyExists -or $after.PolicyValue -ne 1){throw 'Recall policy readback failed.'} $after=Get-WindRecallState;if(-not $after.PolicyExists -or $after.PolicyValue -ne 1){throw 'Recall policy readback failed.'}
+4 -2
View File
@@ -126,7 +126,8 @@ function Get-WindSetupSnapshot {
$options=$Settings.AppOptions $options=$Settings.AppOptions
$sensors=@();if($Settings.DisableSensors){$sensors=@(Get-WindSensorInventory|Where-Object Status -eq 'OK')} $sensors=@();if($Settings.DisableSensors){$sensors=@(Get-WindSensorInventory|Where-Object Status -eq 'OK')}
$recallPlan=if($Settings.Recall){Get-WindRecallPlan -RemoveFeature:$Settings.RemoveRecall}else{$null} $recallPlan=if($Settings.Recall){Get-WindRecallPlan -RemoveFeature:$Settings.RemoveRecall}else{$null}
[pscustomobject]@{Apps=@(Get-WindAppPlan -Inventory $inventory @options);Policies=@(Get-WindPlan -Context (Get-WindContext) -Store (New-WindRegistryStore) -Profile $Settings.Profile -Updates $Settings.Updates);Sensors=$sensors;Recall=$recallPlan} $context=Get-WindContext
[pscustomobject]@{Context=$context;Apps=@(Get-WindAppPlan -Inventory $inventory @options);Policies=@(Get-WindPlan -Context $context -Store (New-WindRegistryStore) -Profile $Settings.Profile -Updates $Settings.Updates);Sensors=$sensors;Recall=$recallPlan}
} }
function Show-WindSetupReview { function Show-WindSetupReview {
param($Snapshot,$Settings) param($Snapshot,$Settings)
@@ -135,7 +136,8 @@ function Show-WindSetupReview {
foreach ($a in @($Snapshot.Apps | Where-Object Status -ne 'Keep')) {$items+=New-WindMenuItem $a.Id ("[$($a.Status)] $($a.Name)") ("$($a.Scope) scope. $($a.Reason). App data may be removed by the uninstaller.") $true} foreach ($a in @($Snapshot.Apps | Where-Object Status -ne 'Keep')) {$items+=New-WindMenuItem $a.Id ("[$($a.Status)] $($a.Name)") ("$($a.Scope) scope. $($a.Reason). App data may be removed by the uninstaller.") $true}
foreach($s in @($Snapshot.Sensors)){$items+=New-WindMenuItem ('sensor:'+ $s.InstanceId) ("[WouldDisable] $($s.Kind): $($s.Name)") $s.Risk $true} foreach($s in @($Snapshot.Sensors)){$items+=New-WindMenuItem ('sensor:'+ $s.InstanceId) ("[WouldDisable] $($s.Kind): $($s.Name)") $s.Risk $true}
if($Snapshot.Recall){foreach($r in $Snapshot.Recall.Items){$items+=New-WindMenuItem ('recall:'+ $r.Id) ("[$($r.Status)] $($r.Action)") $r.Impact $true}} if($Snapshot.Recall){foreach($r in $Snapshot.Recall.Items){$items+=New-WindMenuItem ('recall:'+ $r.Id) ("[$($r.Status)] $($r.Action)") $r.Impact $true}}
Select-WindMenu -Title 'Review your plan' -Subtitle ("$($Settings.Preset) | $($Settings.Profile) | Apps: $(@($Snapshot.Apps|Where-Object Status -eq 'Remove').Count) | Sensors: $(@($Snapshot.Sensors).Count) | Network: $($Settings.Network)") -Items $items -Searchable | Out-Null $available=@($Snapshot.Policies|Where-Object {$_.Status-in@('WouldChange','Configured')}).Count;$unsupported=@($Snapshot.Policies|Where-Object {$_.Status-eq'Unsupported'}).Count
Select-WindMenu -Title 'Review your plan' -Subtitle ("$($Snapshot.Context.Edition) $($Snapshot.Context.Build) | $available policies available / $unsupported unsupported | Apps: $(@($Snapshot.Apps|Where-Object Status -eq 'Remove').Count) | Network: $($Settings.Network)") -Items $items -Searchable | Out-Null
} }
function Set-WindSetupPreset { function Set-WindSetupPreset {
param($Settings,$Preset) param($Settings,$Preset)
+1 -1
View File
@@ -8,5 +8,5 @@ foreach ($file in $files) {
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors) | Out-Null [System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors) | Out-Null
if ($errors.Count) {throw ($errors | Out-String)} if ($errors.Count) {throw ($errors | Out-String)}
} }
foreach ($suite in @('Run-Tests.ps1','Run-AppTests.ps1','Run-OfflineUpdateTests.ps1','Run-FeatureTests.ps1')) {& (Join-Path $PSScriptRoot $suite)} foreach ($suite in @('Run-Tests.ps1','Run-AppTests.ps1','Run-OfflineUpdateTests.ps1','Run-FeatureTests.ps1','Run-CleanupTests.ps1','Run-CaptureTests.ps1')) {& (Join-Path $PSScriptRoot $suite)}
Write-Output 'All checks passed. No live policy or application mutations were performed.' Write-Output 'All checks passed. No live policy or application mutations were performed.'
+43
View File
@@ -0,0 +1,43 @@
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
Import-Module (Join-Path $root 'src/Wind.Network.psm1') -Force
$script:passed=0
function Assert-Capture([bool]$Condition,[string]$Message){if(-not$Condition){throw $Message};$script:passed++}
$state=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-capture-test-'+[guid]::NewGuid().ToString('N'))
try{
$candidate=Join-Path $state 'capture-one'
$resolved=Resolve-WindCaptureDirectory -Path $candidate -StateRoot $state
Assert-Capture ($resolved-eq$candidate-and-not(Test-Path -LiteralPath $candidate)) 'Capture resolution created a directory or changed the target.'
foreach($invalid in @($state,($state+'-sibling\capture'),(Join-Path $state '..\outside'),'relative\capture','C:relative','\relative','\\server\share',($candidate+':stream'))){
$threw=$false;try{Resolve-WindCaptureDirectory -Path $invalid -StateRoot $state|Out-Null}catch{$threw=$true}
Assert-Capture $threw "Unsafe capture path accepted: $invalid"
}
[IO.Directory]::CreateDirectory($state)|Out-Null
$file=Join-Path $state 'file';[IO.File]::WriteAllText($file,'test')
$threw=$false;try{Resolve-WindCaptureDirectory -Path (Join-Path $file 'child') -StateRoot $state|Out-Null}catch{$threw=$true}
Assert-Capture $threw 'Capture path crossed an existing file.'
$calls=[Collections.Generic.List[string]]::new()
$runner={param($NativeArguments)$calls.Add(($NativeArguments-join'|'));0}.GetNewClosure()
Invoke-WindPacketMonitor -ArgumentList @('start','--file-name','path with spaces\capture.etl') -Runner $runner
Assert-Capture ($calls.Count-eq1-and$calls[0]-eq'start|--file-name|path with spaces\capture.etl') 'Packet monitor changed native argument boundaries.'
$threw=$false;try{Invoke-WindPacketMonitor -ArgumentList @('start') -Runner {23}}catch{$threw=$_.Exception.Message-match'exit code 23'}
Assert-Capture $threw 'Packet monitor failure was reported as success.'
$threw=$false;try{Invoke-WindPacketMonitor -ArgumentList @('stop') -Runner {}}catch{$threw=$true}
Assert-Capture $threw 'Missing packet monitor exit code was reported as success.'
# Real entry points must return before privilege checks, directory creation or native commands.
$preview=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) ('MsPrivate\network\preview-test-'+[guid]::NewGuid().ToString('N'))
& (Join-Path $root 'Network.ps1') -Mode CaptureStart -CaptureDirectory $preview -WhatIf
& (Join-Path $root 'Network.ps1') -Mode CaptureStop -CaptureDirectory $preview -WhatIf
Assert-Capture (-not(Test-Path -LiteralPath $preview)) 'Capture preview created state on disk.'
& (Join-Path $root 'Network.ps1') -Mode CaptureConvert -CaptureDirectory $preview -WhatIf
Assert-Capture (-not(Test-Path -LiteralPath $preview)) 'Capture conversion preview created state on disk.'
}finally{
$full=[IO.Path]::GetFullPath($state);$temp=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')+'\'
if(-not$full.StartsWith($temp,[StringComparison]::OrdinalIgnoreCase)){throw 'Unsafe test cleanup path.'}
if([IO.Directory]::Exists($full)){[IO.Directory]::Delete($full,$true)}
}
Write-Output "$script:passed capture tests passed. No packet capture or firewall change was performed."
+68
View File
@@ -0,0 +1,68 @@
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$scratch=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-cleanup-test-'+[guid]::NewGuid().ToString('N'))
$module=Import-Module (Join-Path $root 'src/Wind.Cleanup.psm1') -Force -PassThru
$script:passed=0
function Assert-Cleanup([bool]$Condition,[string]$Message){if(-not$Condition){throw $Message};$script:passed++}
try{
$files=Join-Path $scratch 'files';[IO.Directory]::CreateDirectory($files)|Out-Null
# Use the production allowlist and an exact one-file plan; never scan or select other Temp files.
$tempRoot=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')
$file=Join-Path $files 'sample.txt';$report=Join-Path $scratch 'report.json'
function New-TestCleanupPlan {
[IO.File]::WriteAllText($file,'original')
[IO.File]::SetLastWriteTimeUtc($file,[DateTime]::UtcNow.AddDays(-30))
$item=Get-Item -LiteralPath $file
[pscustomobject]@{
Schema=2;AgeDays=7;CutoffUtc=[DateTime]::UtcNow.AddDays(-7).ToString('o');Categories=@('Temp')
Files=@([pscustomobject]@{Path=$item.FullName;Root=$tempRoot;Category='Temp';Length=$item.Length;LastWriteUtc=$item.LastWriteTimeUtc.ToString('o')})
}
}
$plan=New-TestCleanupPlan
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -WhatIf
Assert-Cleanup ($result.Status-eq'Preview'-and(Test-Path -LiteralPath $file)-and-not(Test-Path -LiteralPath $report)) 'WhatIf changed files or wrote a cleanup report.'
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
Assert-Cleanup ($result.Deleted-eq1-and$result.FreedBytes-eq8-and-not(Test-Path -LiteralPath $file)) 'Unchanged planned file was not deleted and accounted for.'
$plan=New-TestCleanupPlan
[IO.File]::SetLastWriteTimeUtc($file,[DateTime]::UtcNow.AddDays(-20))
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
Assert-Cleanup ($result.Skipped-eq1-and$result.Deleted-eq0-and(Test-Path -LiteralPath $file)) 'File changed after planning was deleted because it was still older than the cutoff.'
$plan=New-TestCleanupPlan;$originalWrite=[IO.File]::GetLastWriteTimeUtc($file)
[IO.File]::WriteAllText($file,'different size');[IO.File]::SetLastWriteTimeUtc($file,$originalWrite)
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
Assert-Cleanup ($result.Skipped-eq1-and(Test-Path -LiteralPath $file)) 'Changed file size was ignored when the timestamp was preserved.'
$plan=New-TestCleanupPlan;$handle=[IO.File]::Open($file,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None)
try{$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false}finally{$handle.Dispose()}
Assert-Cleanup ($result.Skipped-eq1-and$result.Deleted-eq0-and(Test-Path -LiteralPath $file)) 'Locked file was not recorded as skipped.'
$plan=New-TestCleanupPlan;$plan.Files[0].LastWriteUtc='invalid'
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
Assert-Cleanup ($result.Skipped-eq1-and(Test-Path -LiteralPath $file)) 'Invalid planned timestamp did not preserve the file.'
# Exercise the real entry script with mock dependencies; never read or clear the real clipboard.
$wrapper=Join-Path $scratch 'wrapper';$src=Join-Path $wrapper 'src';[IO.Directory]::CreateDirectory($src)|Out-Null
Copy-Item -LiteralPath (Join-Path $root 'Cleanup.ps1') -Destination $wrapper
[IO.File]::WriteAllText((Join-Path $src 'Wind.Core.psm1'),'function Assert-WindMutationAllowed {}')
$mock=@'
function Get-WindCleanupPlan { [pscustomobject]@{Files=@()} }
function Invoke-WindCleanup { throw 'Unexpected cleanup mutation during WhatIf.' }
function Clear-WindCurrentClipboard { throw 'Unexpected clipboard mutation during WhatIf.' }
'@
[IO.File]::WriteAllText((Join-Path $src 'Wind.Cleanup.psm1'),$mock)
& (Join-Path $wrapper 'Cleanup.ps1') -Mode Run -ClearClipboard -WhatIf
Assert-Cleanup $true 'Cleanup entry script WhatIf did not return before mutations.'
}finally{
Remove-Module $module -ErrorAction SilentlyContinue
$resolved=[IO.Path]::GetFullPath($scratch)
$temp=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')+'\'
if(-not$resolved.StartsWith($temp,[StringComparison]::OrdinalIgnoreCase)){throw 'Unsafe test cleanup directory.'}
if([IO.Directory]::Exists($resolved)){[IO.Directory]::Delete($resolved,$true)}
}
Write-Output "$script:passed cleanup tests passed. Only disposable files and a mocked clipboard were used."
+34
View File
@@ -22,6 +22,26 @@ try{
$saved=Get-Content -LiteralPath $jsonPath -Raw|ConvertFrom-Json $saved=Get-Content -LiteralPath $jsonPath -Raw|ConvertFrom-Json
Assert-True ($saved.Value-eq2-and-not@(Get-ChildItem -LiteralPath $ioRoot -Filter '*.tmp' -Force).Count-and-not@(Get-ChildItem -LiteralPath $ioRoot -Filter '*.previous' -Force).Count) 'Atomic JSON replacement left stale state or temporary files.' Assert-True ($saved.Value-eq2-and-not@(Get-ChildItem -LiteralPath $ioRoot -Filter '*.tmp' -Force).Count-and-not@(Get-ChildItem -LiteralPath $ioRoot -Filter '*.previous' -Force).Count) 'Atomic JSON replacement left stale state or temporary files.'
$probePath=Join-Path $ioRoot 'Probe.ps1';[IO.File]::WriteAllText($probePath,"param([string]`$Value) [IO.File]::WriteAllText((Join-Path `$PSScriptRoot 'probe.txt'),`$Value)",[Text.UTF8Encoding]::new($false))
$invoker=New-MsPrivateScriptInvoker -Root $ioRoot
$action=({& $invoker 'Probe.ps1' @{Value='nested-module'}}.GetNewClosure())
$invokerRun=Invoke-MsPrivateGroupPlan -Groups @([pscustomobject]@{Id='Invoker';Label='Captured script invoker';DependsOn=@();Action=$action})
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.'
[IO.File]::WriteAllText((Join-Path $ioRoot 'Output.ps1'),'param([string]$Value) [pscustomobject]@{Value=$Value}')
$output=& $invoker 'Output.ps1' @{Value='visible-audit'} -PassThru
Assert-True ($output.Value-eq'visible-audit') 'GUI script invoker discarded requested output.'
$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 $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.' $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 $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
@@ -70,6 +90,20 @@ $run=Invoke-MsPrivateGroupPlan -Groups $groups
Assert-True ($run.Status-eq'PartialFailure'-and($run.Entries | Where-Object { $_.Id -eq 'First' }).Status-eq'Failed') 'Operation runner did not record a failed group.' Assert-True ($run.Status-eq'PartialFailure'-and($run.Entries | Where-Object { $_.Id -eq 'First' }).Status-eq'Failed') 'Operation runner did not record a failed group.'
Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Dependent' }).Status-eq'SkippedDependency'-and$events-notcontains'dependent') 'Operation runner executed a blocked dependency.' Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Dependent' }).Status-eq'SkippedDependency'-and$events-notcontains'dependent') 'Operation runner executed a blocked dependency.'
Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Independent' }).Status-eq'Succeeded'-and$events-contains'independent') 'Operation runner did not continue with an independent group.' Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Independent' }).Status-eq'Succeeded'-and$events-contains'independent') 'Operation runner did not continue with an independent group.'
$caseRun=Invoke-MsPrivateGroupPlan -Groups @(
[pscustomobject]@{Id='First';Label='First';DependsOn=@();Action={}},
[pscustomobject]@{Id='Second';Label='Second';DependsOn=@('first');Action={}}
)
Assert-True ($caseRun.Status-eq'Completed'-and$caseRun.Entries[1].Status-eq'Succeeded') 'A dependency accepted during validation was incorrectly skipped due to capitalization.'
$runReportRoot=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-run-report-'+[guid]::NewGuid().ToString('N'))
try{
$runReportPath=Join-Path $runReportRoot 'run.json'
$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() $calls=[Collections.Generic.List[string]]::new()
$backend=@{ $backend=@{
+30
View File
@@ -0,0 +1,30 @@
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$scratch=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-gui-test-'+[guid]::NewGuid().ToString('N'))
try{
[IO.Directory]::CreateDirectory($scratch)|Out-Null
foreach($folder in @('src','config')){Copy-Item -LiteralPath (Join-Path $root $folder) -Destination $scratch -Recurse}
$source=[IO.File]::ReadAllText((Join-Path $root 'ms-private-GUI.ps1'))
$marker='[void]$form.ShowDialog()'
if(([regex]::Matches($source,[regex]::Escape($marker))).Count-ne1){throw 'GUI smoke test requires exactly one top-level window entry point.'}
# Construct the real controls and event handlers without opening a window or clicking mutation actions.
$check=@'
try{
if($tabs.TabPages.Count-ne6){throw 'Expected six GUI tabs.'}
if($capturePath.Parent-ne$network-or$stopCapture.Parent-ne$network-or$convertCapture.Parent-ne$network){throw 'Capture controls are not attached to the network tab.'}
if($stopCapture.Tag-ne'CaptureStop'-or$convertCapture.Tag-ne'CaptureConvert'){throw 'Capture controls target the wrong commands.'}
if($securityAudit.Parent-ne$maintenance){throw 'Security audit button is missing.'}
if($homeTab.Controls.Count-lt9){throw 'Home guide controls are missing.'}
Write-Output 'GUI construction smoke check passed. No window actions were invoked.'
}finally{$form.Dispose()}
'@
$path=Join-Path $scratch 'ms-private-GUI.ps1'
[IO.File]::WriteAllText($path,$source.Replace($marker,$check),[Text.UTF8Encoding]::new($false))
& $path
}finally{
$temp=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')+'\'
if(-not[IO.Path]::GetFullPath($scratch).StartsWith($temp,[StringComparison]::OrdinalIgnoreCase)){throw 'Unsafe GUI test cleanup path.'}
if([IO.Directory]::Exists($scratch)){[IO.Directory]::Delete($scratch,$true)}
}
+7 -5
View File
@@ -45,12 +45,14 @@ function New-Plan { param($Harness,[string[]]$Ids=@('advertising-id','clipboard-
function Apply-Test { param($Harness,$Plan) Invoke-WindTransaction -Plan $Plan -Context $Harness.Context -Store $Harness.Store -Save $Harness.Save -InstallationId 'test-installation' } function Apply-Test { param($Harness,$Plan) Invoke-WindTransaction -Plan $Plan -Context $Harness.Context -Store $Harness.Store -Save $Harness.Save -InstallationId 'test-installation' }
function Restore-Test { param($Harness,$Journal) Restore-WindTransaction -Journal $Journal -Store $Harness.Store -Save $Harness.Save -InstallationId 'test-installation' } function Restore-Test { param($Harness,$Journal) Restore-WindTransaction -Journal $Journal -Store $Harness.Store -Save $Harness.Save -InstallationId 'test-installation' }
Run-Test 'Home is unsupported even if a matching policy value exists' { Run-Test 'Home applies only explicitly documented direct registry controls' {
$h=New-Harness; $h.Context.Edition='Core' $h=New-Harness; $h.Context.Edition='Core'
$plan=New-Plan $h $plan=@(Get-WindPlan -Context $h.Context -Store $h.Store -Id @('advertising-id','implicit-text-collection','windows-error-reporting'))
Assert-True (@($plan | Where-Object Status -NE 'Unsupported').Count -eq 0) 'Home policies must not be called enforced.' Assert-True (($plan|Where-Object Id -EQ 'advertising-id').Status-eq'Unsupported') 'Managed-edition policy was offered on Home.'
Assert-Throws { Apply-Test $h $plan } $eligible=@($plan|Where-Object Status -EQ 'WouldChange')
Assert-True ($h.State.Writes -eq 0 -and $h.State.Saves -eq 0) 'Unsupported apply touched state.' Assert-True ($eligible.Id-contains'implicit-text-collection'-and$eligible.Id-contains'windows-error-reporting') 'Documented direct registry controls were not offered on Home.'
$j=Apply-Test $h $eligible
Assert-True ($j.Status-eq'Applied'-and$h.State.Writes-eq2) 'Home direct registry controls were not applied transactionally.'
} }
Run-Test 'Unknown build is audit-only' { Run-Test 'Unknown build is audit-only' {
$h=New-Harness; $h.Context.Build=99999 $h=New-Harness; $h.Context.Build=99999