Prepare ms-private 0.0.1 release candidate

This commit is contained in:
0x000 committed 2026-09-22 21:21:41 -04:00
1 parent 7aacc0b63f
commit a515538438
75 files changed
+3957 -3484

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
* text=auto
*.ps1 text eol=crlf
*.psm1 text eol=crlf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
+12 -2
View File
@@ -3,5 +3,15 @@ reports/
*.tmp *.tmp
audit-preview.json audit-preview.json
node_modules/ node_modules/
outputs/**/preview-*.png **/bin/
outputs/**/*.inspect.ndjson **/obj/
dist/
*.zip
*.exe
*.apbx
*.xlsx.inspect.ndjson
*.log
.idea/
.vscode/
.DS_Store
Thumbs.db
+32
View File
@@ -0,0 +1,32 @@
stages:
- test
- build
default:
tags:
- windows
test:windows-powershell:
stage: test
script:
- powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\tests\Run-All.ps1
test:powershell-7:
stage: test
script:
- pwsh.exe -NoProfile -ExecutionPolicy Bypass -File .\tests\Run-All.ps1
build:executable:
stage: build
needs:
- test:windows-powershell
- test:powershell-7
script:
- powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Build-Executable.ps1 -OutputDirectory .\dist\ci -Version 0.0.1
artifacts:
name: ms-private-0.0.1
paths:
- dist/ci/ms-private-0.0.1.exe
- dist/ci/ms-private-0.0.1.exe.sha256
- dist/ci/SHA256SUMS.txt
expire_in: 30 days
+12
View File
@@ -0,0 +1,12 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('Audit','Plan','Apply','OpenSettings')][string]$Mode='Plan',[ValidateSet('Consumer','AllAppAccounts')][string]$Scope='AllAppAccounts',[switch]$AcknowledgeAccountDataDeletion,[switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Accounts.psm1') -Force
if($Mode-eq'OpenSettings'){Start-Process 'ms-settings:emailandaccounts';Start-Process 'ms-settings:workplace';return}
$backend=New-MsPrivateAccountBackend;$plan=@(Get-MsPrivateAccountPlan -Scope $Scope -Backend $backend)
if($Mode-in@('Audit','Plan')){$safe=@($plan|Select-Object Id,Kind,Label,Status,Impact);if($Json){[pscustomobject]@{Schema=1;Scope=$Scope;Warning='Package reset deletes current-user app data and sign-in details. Credential targets and secrets are omitted.';Items=$safe}|ConvertTo-Json -Depth 6}else{$safe|Format-Table -Wrap};return}
if(-not$AcknowledgeAccountDataDeletion){throw 'Apply requires -AcknowledgeAccountDataDeletion. App data, tokens and matching stored credentials cannot be restored by ms-private.'}
Assert-WindMutationAllowed
if(-not$PSCmdlet.ShouldProcess('Current-user Microsoft application accounts and matching stored credentials','Permanently sign out and delete local account data')){return}
$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\accounts';[IO.Directory]::CreateDirectory($root)|Out-Null;$reportPath=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');$save={param($x)Save-MsPrivateAccountReport -Report $x -Path $reportPath}.GetNewClosure()
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.'
+19
View File
@@ -0,0 +1,19 @@
#Requires -Version 5.1
[CmdletBinding()]param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.1')
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
if(-not$OutputDirectory){$OutputDirectory=Join-Path $PSScriptRoot 'dist'};$out=[IO.Path]::GetFullPath($OutputDirectory);[IO.Directory]::CreateDirectory($out)|Out-Null
$seven=@((Get-Command 7z.exe -ErrorAction SilentlyContinue),(Get-Command (Join-Path $env:ProgramFiles '7-Zip\7z.exe') -ErrorAction SilentlyContinue))|Where-Object{$_}|Select-Object -First 1
if(-not$seven){throw '7-Zip is required to create an AME .apbx archive.'}
$stage=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-ame-'+[guid]::NewGuid().ToString('N'));[IO.Directory]::CreateDirectory($stage)|Out-Null
try{
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'ame\src\playbook.conf') -Destination $stage
$playbookPath=Join-Path $stage 'playbook.conf';$playbook=[xml][IO.File]::ReadAllText($playbookPath);$playbook.Playbook.Version=$Version;$settings=[Xml.XmlWriterSettings]::new();$settings.Encoding=[Text.UTF8Encoding]::new($false);$settings.Indent=$true;$writer=[Xml.XmlWriter]::Create($playbookPath,$settings);try{$playbook.Save($writer)}finally{$writer.Dispose()}
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'ame\src\Configuration') -Destination $stage -Recurse
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'ame\src\Executables') -Destination $stage -Recurse
$payload=Join-Path $stage 'Executables\ms-private'
foreach($file in @(Get-ChildItem -LiteralPath $PSScriptRoot -File|Where-Object{$_.Extension-in@('.ps1','.md')})) {Copy-Item -LiteralPath $file.FullName -Destination $payload}
foreach($folder in @('src','config','docs')){Copy-Item -LiteralPath (Join-Path $PSScriptRoot $folder) -Destination $payload -Recurse}
$target=Join-Path $out ("ms-private-$Version.apbx");if(Test-Path -LiteralPath $target){throw 'AME archive already exists. Use an empty output directory.'}
Push-Location $stage;try{& $seven.Source a $target playbook.conf Executables Configuration '-pmalte'|Out-Host;if($LASTEXITCODE-ne 0){throw "7-Zip failed with exit code $LASTEXITCODE."}}finally{Pop-Location}
$hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant();[IO.File]::WriteAllText((Join-Path $out ("ms-private-$Version.apbx.sha256")),($hash+' '+[IO.Path]::GetFileName($target)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false));Write-Output "Created $target. SHA256: $hash"
}finally{if(Test-Path -LiteralPath $stage){Remove-Item -LiteralPath $stage -Recurse -Force}}
+38
View File
@@ -0,0 +1,38 @@
#Requires -Version 5.1
[CmdletBinding()]param([string]$OutputDirectory,[string]$Version='0.0.1')
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
if($Version-notmatch'^\d+\.\d+\.\d+$'){throw 'Version must use major.minor.patch.'}
$root=[IO.Path]::GetFullPath($PSScriptRoot);if(-not$OutputDirectory){$OutputDirectory=Join-Path $root ('dist\ms-private-'+$Version+'-final-'+(Get-Date -Format yyyyMMdd))};$output=[IO.Path]::GetFullPath($OutputDirectory);[IO.Directory]::CreateDirectory($output)|Out-Null
$target=Join-Path $output ("ms-private-$Version.exe");if(Test-Path -LiteralPath $target){throw 'Executable already exists. Use an empty output directory.'}
$dotnet=(Get-Command dotnet.exe -CommandType Application -ErrorAction Stop).Source
$temp=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-native-'+[guid]::NewGuid().ToString('N'));[IO.Directory]::CreateDirectory($temp)|Out-Null
try{
$payload=Join-Path $temp 'ms-private.payload.zip';$entries=[Collections.Generic.List[object]]::new()
$rootScripts=@('Accounts.ps1','Cleanup.ps1','Hardware.ps1','Home.ps1','Install-Apps.ps1','Lock-Event.ps1','Lock.ps1','Manage-Apps.ps1','ms-private-GUI.ps1','ms-private.ps1','Network.ps1','Offline-Updates.ps1','Preset.ps1','Recall.ps1','Security.ps1','Services.ps1')
$files=@($rootScripts|ForEach-Object{Get-Item -LiteralPath (Join-Path $root $_)})
foreach($folder in @('src','config')){$files+=@(Get-ChildItem -LiteralPath (Join-Path $root $folder) -Recurse -File -Force)}
foreach($doc in @('docs\HOME-GUIDE.md','docs\PRIVACY.md','docs\ANONYMITY.md','docs\TOOL-BREAKDOWN.md','docs\OFFLINE-UPDATES.md','docs\NATIVE-API.md','docs\ROADMAP.md')){$files+=Get-Item -LiteralPath (Join-Path $root $doc)}
Add-Type -AssemblyName System.IO.Compression;Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip=[IO.Compression.ZipFile]::Open($payload,[IO.Compression.ZipArchiveMode]::Create)
try{
foreach($file in @($files|Sort-Object FullName)){
if(-not$file.FullName.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase)-or($file.Attributes-band[IO.FileAttributes]::ReparsePoint)){throw 'Unsafe executable payload input.'}
$relative=$file.FullName.Substring($root.Length+1).Replace('\','/');$hash=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant();$entries.Add([ordered]@{Path=$relative;Length=[long]$file.Length;Sha256=$hash})
[IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,$file.FullName,$relative,[IO.Compression.CompressionLevel]::Optimal)|Out-Null
}
$manifest=[ordered]@{Schema=1;Version=$Version;EntryPoint='ms-private-GUI.ps1';Files=@($entries)}|ConvertTo-Json -Depth 5 -Compress
$entry=$zip.CreateEntry('payload.manifest.json',[IO.Compression.CompressionLevel]::Optimal);$stream=$entry.Open();try{$writer=[IO.StreamWriter]::new($stream,[Text.UTF8Encoding]::new($false));try{$writer.Write($manifest)}finally{$writer.Dispose()}}finally{$stream.Dispose()}
}finally{$zip.Dispose()}
$appManifest=Join-Path $temp 'app.manifest';$appManifestText=[IO.File]::ReadAllText((Join-Path $root 'native\MsPrivate.Launcher\app.manifest'));$appManifestText=[regex]::Replace($appManifestText,'(?<=<assemblyIdentity version=")[^"]+',($Version+'.0'));[IO.File]::WriteAllText($appManifest,$appManifestText,[Text.UTF8Encoding]::new($false))
$testObj=Join-Path $temp 'test-obj\';$testBin=Join-Path $temp 'test-bin\'
& $dotnet run --project (Join-Path $root 'native\MsPrivate.Launcher.Tests\MsPrivate.Launcher.Tests.csproj') -c Release --nologo "-p:BaseIntermediateOutputPath=$testObj" "-p:BaseOutputPath=$testBin";if($LASTEXITCODE-ne0){throw 'Native launcher tests failed.'}
$publish=Join-Path $temp 'publish';$arguments=@('publish',(Join-Path $root 'native\MsPrivate.Launcher\MsPrivate.Launcher.csproj'),'-c','Release','-r','win-x64','--self-contained','true','--source','https://api.nuget.org/v3/index.json','--nologo','-o',$publish,"-p:PayloadZip=$payload","-p:ApplicationManifest=$appManifest","-p:Version=$Version","-p:AssemblyVersion=$Version.0","-p:FileVersion=$Version.0","-p:BaseIntermediateOutputPath=$(Join-Path $temp 'obj\')","-p:BaseOutputPath=$(Join-Path $temp 'bin\')")
& $dotnet @arguments;if($LASTEXITCODE-ne0){throw 'Single-file publish failed.'}
$published=@(Get-ChildItem -LiteralPath $publish -File);$exe=@($published|Where-Object Extension -eq '.exe');if($exe.Count-ne1-or$published.Count-ne1){throw "Expected one executable; publish produced $($published.Count) files."}
Copy-Item -LiteralPath $exe[0].FullName -Destination $target
$hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant();[IO.File]::WriteAllText((Join-Path $output ("ms-private-$Version.exe.sha256")),($hash+' '+[IO.Path]::GetFileName($target)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false))
& (Join-Path $root 'tests\Inspect-Executable.ps1') -Path $target -Version $Version
$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 $target ($($entries.Count) embedded files). SHA256: $hash"
}finally{if(Test-Path -LiteralPath $temp){[IO.Directory]::Delete($temp,$true)}}
+29
View File
@@ -0,0 +1,29 @@
#Requires -Version 5.1
[CmdletBinding()]
param([string]$OutputDirectory,[ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version='0.0.1')
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
if (-not $OutputDirectory) { $OutputDirectory=Join-Path $PSScriptRoot 'dist' }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$output=[IO.Path]::GetFullPath($OutputDirectory)
[IO.Directory]::CreateDirectory($output) | Out-Null
$zipPath=Join-Path $output ("ms-private-$Version.zip")
if (Test-Path -LiteralPath $zipPath) {throw 'Archive already exists. Choose an empty output directory to avoid overwriting a release.'}
$root=[IO.Path]::GetFullPath($PSScriptRoot)
# Explicit source folders: never include local reports, research archives or dependencies.
$files=@(Get-ChildItem -LiteralPath $root -File | Where-Object { $_.Extension -in @('.ps1','.md') -or $_.Name -in @('.gitignore','.gitattributes','.gitlab-ci.yml') })
foreach ($folder in @('src','config','tests','docs','ame','native')) {
$files+=@(Get-ChildItem -LiteralPath (Join-Path $root $folder) -Recurse -File -Force | Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' })
}
$zip=[IO.Compression.ZipFile]::Open($zipPath,[IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($file in @($files | Sort-Object FullName)) {
if (-not $file.FullName.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase) -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint)) {throw 'Unsafe release input.'}
$relative=$file.FullName.Substring($root.Length+1).Replace('\','/')
[IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,$file.FullName,('ms-private/'+$relative),[IO.Compression.CompressionLevel]::Optimal) | Out-Null
}
} finally {$zip.Dispose()}
$hash=(Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText((Join-Path $output 'SHA256SUMS.txt'),($hash+' '+[IO.Path]::GetFileName($zipPath)+[Environment]::NewLine),[Text.UTF8Encoding]::new($false))
Write-Output "Created $zipPath ($($files.Count) source files). SHA256: $hash"
+11
View File
@@ -0,0 +1,11 @@
# Contributing
Use Windows PowerShell 5.1-compatible syntax. Keep the menu independent from system changes; core operations must be callable and testable without interactive input.
Run `tests/Run-All.ps1` in both Windows PowerShell 5.1 and PowerShell 7. Do not use real app uninstallers or production registry writes in automated tests. Add a regression for a demonstrated failure path, including nonzero native exit codes and interrupted operations.
Each new privacy control needs a primary source, edition/build scope, concrete impact, original-state capture and behavioral validation. A registry match is not proof that Windows enforces a policy. Do not add security exclusions, broad package-removal wildcards, arbitrary command-string execution, forced Edge removal or identifier spoofing.
Keep personal reports, test captures, release binaries, local paths and credentials out of the repository. The reviewed privacy register and blog draft live under `docs`; generated intermediate data stays untracked.
Before publishing a stable release, complete the live validation matrix, select a project license and record any signing/release provenance. A passing CI badge alone is insufficient.
+11
View File
@@ -0,0 +1,11 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('Plan','Run','InstallSchedule','RemoveSchedule')][string]$Mode='Plan',[ValidateRange(0,365)][int]$AgeDays=7,[string[]]$Category=@('Temp'),[ValidateSet('Daily','Weekly')][string]$Frequency='Daily',[switch]$ClearClipboard,[switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Cleanup.psm1')
$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}
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}
$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}
Assert-WindMutationAllowed
if($Mode-eq'InstallSchedule'){if($AgeDays-lt 1){throw 'Scheduled cleanup requires AgeDays of at least 1.'};if($PSCmdlet.ShouldProcess('Task Scheduler','Install safe cleanup schedule')){Install-WindCleanupSchedule -AgeDays $AgeDays -Frequency $Frequency -SourceRoot $PSScriptRoot};return}
if($PSCmdlet.ShouldProcess('Task Scheduler','Remove safe cleanup schedule')){Remove-WindCleanupSchedule}
+8
View File
@@ -0,0 +1,8 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('List','Plan','Apply','Restore')][string]$Mode='List',[string[]]$InstanceId=@(),[string]$Journal,[switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Hardware.psm1')
if($Mode -eq 'List'){$x=@(Get-WindSensorInventory);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
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
+7
View File
@@ -0,0 +1,7 @@
#Requires -Version 5.1
[CmdletBinding()]param([ValidateSet('Guide','Privacy','Location','Camera','Microphone','Diagnostics','Recall','Updates','Apps','DefaultApps')][string]$Page='Guide')
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
$routes=@{Privacy='ms-settings:privacy';Location='ms-settings:privacy-location';Camera='ms-settings:privacy-webcam';Microphone='ms-settings:privacy-microphone';Diagnostics='ms-settings:privacy-feedback';Recall='ms-settings:privacy-recall';Updates='ms-settings:windowsupdate';Apps='ms-settings:appsfeatures';DefaultApps='ms-settings:defaultapps'}
if($Page-eq'Guide'){Get-Content -LiteralPath (Join-Path $PSScriptRoot 'docs/HOME-GUIDE.md') -Encoding UTF8;return}
Start-Process $routes[$Page]
Write-Output "Opened Windows Settings: $Page. ms-private does not claim a result until you verify the displayed toggle."
+3 -1
View File
@@ -12,7 +12,9 @@ foreach ($name in @($App | Select-Object -Unique)) {
if ($PSCmdlet.ShouldProcess($package.Id, 'Install from exact WinGet community-source package; retain installer prompts')) { if ($PSCmdlet.ShouldProcess($package.Id, 'Install from exact WinGet community-source package; retain installer prompts')) {
$winget = Get-Command winget.exe -ErrorAction Stop $winget = Get-Command winget.exe -ErrorAction Stop
# No bypassed hash checks, remote PowerShell execution, automatic source/license acceptance or forced restart. # No bypassed hash checks, remote PowerShell execution, automatic source/license acceptance or forced restart.
& $winget.Source install --id $package.Id --exact --source winget --interactive & $winget.Source install --id $package.Id --exact --source winget --interactive --disable-interactivity
if ($LASTEXITCODE -ne 0) { throw "WinGet returned $LASTEXITCODE for $name. Installation was not confirmed; inspect its output." } if ($LASTEXITCODE -ne 0) { throw "WinGet returned $LASTEXITCODE for $name. Installation was not confirmed; inspect its output." }
& $winget.Source list --id $package.Id --exact --source winget --disable-interactivity | Out-Host
if ($LASTEXITCODE -ne 0) { throw "$name was not found after the installer returned success." }
} }
} }
+5
View File
@@ -0,0 +1,5 @@
#Requires -Version 5.1
param([Parameter(Mandatory)][string]$Config)
$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Lock.psm1')
$cfg=Get-Content -LiteralPath $Config -Raw|ConvertFrom-Json;if($cfg.Schema-ne 1){throw 'Invalid lock configuration.'}
$report=Join-Path $PSScriptRoot ('last-event-'+(Get-Date -Format 'yyyyMMdd-HHmmss')+'.json');Invoke-WindLockEvent -VeraCryptVolume @($cfg.VeraCryptVolume) -VeraCryptPath ([string]$cfg.VeraCryptPath) -ReportPath $report|Out-Null
+5
View File
@@ -0,0 +1,5 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('Install','Remove','Test')][string]$Mode='Install',[string[]]$VeraCryptVolume=@(),[string]$VeraCryptPath)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Lock.psm1')
if($Mode-eq'Test'){$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\lock';[IO.Directory]::CreateDirectory($root)|Out-Null;Invoke-WindLockEvent -VeraCryptVolume $VeraCryptVolume -VeraCryptPath $VeraCryptPath -ReportPath (Join-Path $root 'manual-test.json')|Format-List;return}
Assert-WindMutationAllowed;if($Mode-eq'Install'){if($PSCmdlet.ShouldProcess('Task Scheduler','Install session-lock privacy handler')){Install-WindLockHandler -VeraCryptVolume $VeraCryptVolume -VeraCryptPath $VeraCryptPath -SourceRoot $PSScriptRoot}}else{if($PSCmdlet.ShouldProcess('Task Scheduler','Remove session-lock privacy handler')){Remove-WindLockHandler}}
+21 -10
View File
@@ -2,19 +2,28 @@
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
param( param(
[ValidateSet('List','Plan','Apply')][string]$Mode = 'Plan', [ValidateSet('List','Plan','Apply')][string]$Mode = 'Plan',
[ValidateSet('Zen','Helium','Tor')][string[]]$Browser = @('Zen'), [string[]]$Browser = @('Zen'),
[string[]]$ExtraId = @(), [string[]]$ExcludeId = @(), [string[]]$ExtraId = @(), [string[]]$ExcludeId = @(),
[switch]$SkipBundled, [switch]$KeepOneDrive, [switch]$KeepEdge, [switch]$Json [switch]$SkipBundled, [switch]$KeepOneDrive, [switch]$KeepEdge,
[switch]$RemovePersonal, [switch]$RemoveStore, [switch]$Json,
[Parameter(DontShow)][object[]]$ReviewedPlan
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1') -Force Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1')
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Apps.psm1') -Force Import-Module (Join-Path $PSScriptRoot 'src/Wind.Apps.psm1')
$Browser=@($Browser|ForEach-Object{$_.Split(',')}|ForEach-Object{$_.Trim()}|Where-Object{$_}|Select-Object -Unique)
if(@($Browser|Where-Object{$_ -notin @('Zen','Helium','Tor')}).Count){throw 'Browser must contain only Zen, Helium or Tor.'}
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' }
if ($Json -and $Mode -eq 'Apply') { throw '-Json is for List/Plan. Apply displays installer output.' } if ($Json -and $Mode -eq 'Apply') { throw '-Json is for List/Plan. Apply displays installer output.' }
$inventory = @(Get-WindAppInventory) $inventory = @(Get-WindAppInventory)
$selection = @{ ExtraId=$ExtraId; ExcludeId=$ExcludeId; SkipBundled=$SkipBundled; KeepOneDrive=$KeepOneDrive; KeepEdge=$KeepEdge } $selection = @{ ExtraId=$ExtraId; ExcludeId=$ExcludeId; SkipBundled=$SkipBundled; KeepOneDrive=$KeepOneDrive; KeepEdge=$KeepEdge; RemovePersonal=$RemovePersonal; RemoveStore=$RemoveStore }
$plan = @(Get-WindAppPlan -Inventory $inventory @selection) $plan = @(Get-WindAppPlan -Inventory $inventory @selection)
if ($PSBoundParameters.ContainsKey('ReviewedPlan')) {
$reviewed = @($ReviewedPlan | Where-Object Status -eq 'Remove' | Sort-Object Id | Select-Object Id,Name,Version,Publisher,Kind,Scope,Target)
$fresh = @($plan | Where-Object Status -eq 'Remove' | Sort-Object Id | Select-Object Id,Name,Version,Publisher,Kind,Scope,Target)
if ((ConvertTo-Json -InputObject $reviewed -Compress) -cne (ConvertTo-Json -InputObject $fresh -Compress)) { throw 'The app plan changed since review. Review the refreshed list before applying.' }
}
if ($Mode -eq 'List') { if ($Mode -eq 'List') {
if ($Json) { ConvertTo-Json -InputObject @($plan) -Depth 5 } if ($Json) { ConvertTo-Json -InputObject @($plan) -Depth 5 }
else { $plan | Select-Object Name,Status,Scope,Id,Reason | Format-Table -Wrap } else { $plan | Select-Object Name,Status,Scope,Id,Reason | Format-Table -Wrap }
@@ -27,21 +36,23 @@ if ($Json) {
Write-Output ('Browsers: ' + ($Browser -join ', ')) Write-Output ('Browsers: ' + ($Browser -join ', '))
$plan | Where-Object Status -ne 'Keep' | Select-Object Name,Status,Scope,Reason | Format-Table -Wrap $plan | Where-Object Status -ne 'Keep' | Select-Object Name,Status,Scope,Reason | Format-Table -Wrap
Write-Output 'Removal can delete app settings/data. Export mail, browser data and unsynced work first. OneDrive cloud-only files remain online; folders are not manually deleted.' Write-Output 'Removal can delete app settings/data. Export mail, browser data and unsynced work first. OneDrive cloud-only files remain online; folders are not manually deleted.'
Write-Output 'Store, WebView2, runtimes, security tools and drivers are protected. Appx removal affects this user; provisioning and other users are unchanged.' Write-Output $(if($RemoveStore){'Microsoft Store is selected for current-user removal. Reinstallation may require an official recovery path.'}else{'Microsoft Store is protected unless -RemoveStore is explicitly selected.'})
Write-Output 'WebView2, runtimes, security tools, drivers, servicing and recovery components remain protected. Appx removal affects this user; provisioning and other users are unchanged.'
if ($Mode -eq 'Plan') { return } if ($Mode -eq 'Plan') { return }
if (-not $PSCmdlet.ShouldProcess('Displayed app plan', 'Install selected browsers and remove selected apps')) { return } if (-not $PSCmdlet.ShouldProcess('Displayed app plan', 'Install selected browsers and remove selected apps')) { return }
Assert-WindMutationAllowed Assert-WindMutationAllowed
$context = Get-WindContext $context = Get-WindContext
if ($context.Build -notin @(19045,22631,26100,26200)) { throw 'App changes are gated to Windows builds 19045, 22631, 26100 and 26200. Use Windows Installed apps on other builds.' } if ($context.Build -notin @(19045,22631,26100,26200)) { throw 'App changes are gated to Windows builds 19045, 22631, 26100 and 26200. Use Windows Installed apps on other builds.' }
$mutex = [Threading.Mutex]::new($false,'Global\WindPrivacyPolicyTransaction') $mutex = [Threading.Mutex]::new($false,'Global\MsPrivatePolicyTransaction')
$locked = $false $locked = $false
try { try {
try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true } try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true }
if (-not $locked) { throw 'Another Wind transaction is running.' } if (-not $locked) { throw 'Another ms-private transaction is running.' }
$reportRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'WindPrivacy\apps' $reportRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\apps'
[IO.Directory]::CreateDirectory($reportRoot) | Out-Null [IO.Directory]::CreateDirectory($reportRoot) | Out-Null
$reportPath = Join-Path $reportRoot ((Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N') + '.json') $reportPath = Join-Path $reportRoot ((Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N') + '.json')
$save = { param($data) Save-WindJournal -Journal $data -Path $reportPath }.GetNewClosure() $journalWriter = Get-Command Save-WindJournal -CommandType Function
$save = { param($data) & $journalWriter -Journal $data -Path $reportPath }.GetNewClosure()
Write-Output "Operation report: $reportPath" Write-Output "Operation report: $reportPath"
$result = Invoke-WindAppChanges -Plan $plan -Browser $Browser -Backend (New-WindAppBackend) -Save $save -Confirm:$false $result = Invoke-WindAppChanges -Plan $plan -Browser $Browser -Backend (New-WindAppBackend) -Save $save -Confirm:$false
Write-Output "App operation: $($result.Status). Set your default browser in Windows Settings and verify it opens links." Write-Output "App operation: $($result.Status). Set your default browser in Windows Settings and verify it opens links."
+44
View File
@@ -0,0 +1,44 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateSet('Plan','Apply','Restore','Confirm','Validate','CaptureStart','CaptureStop')][string]$Mode='Plan',
[ValidateSet('Observe','BlockOptionalTelemetry','Isolate')][string]$Profile='Observe',
[int]$RollbackMinutes=10,[string]$Journal,[string]$CaptureDirectory,[switch]$FromRecoveryTask,[switch]$Json
)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1')
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Network.psm1')
$stateRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\network'
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 'CaptureStart'){
Assert-WindMutationAllowed;if(-not (Get-Command pktmon.exe -ErrorAction SilentlyContinue)){throw 'pktmon.exe is unavailable.'}
if(-not $CaptureDirectory){$CaptureDirectory=Join-Path $stateRoot ('capture-'+(Get-Date -Format 'yyyyMMdd-HHmmss'))}
[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
[pscustomobject]@{Status='Running';Directory=$CaptureDirectory;Warning='Capture can contain private addresses and DNS names. Stop and delete or export it explicitly.'}|ConvertTo-Json
return
}
if($Mode -eq 'CaptureStop'){
if(-not $CaptureDirectory){throw 'CaptureStop requires -CaptureDirectory.'};$full=[IO.Path]::GetFullPath($CaptureDirectory)
if(-not $full.StartsWith([IO.Path]::GetFullPath($stateRoot)+'\',[StringComparison]::OrdinalIgnoreCase)){throw 'Capture directory must be inside the ms-private local state directory.'}
& 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}
Save-WindJournal -Journal (Get-WindNetworkValidation) -Path (Join-Path $full 'connections.json')
Write-Output "Capture stopped: $full";return
}
Assert-WindMutationAllowed
if($Mode -eq 'Apply'){
if($Profile -eq 'Observe'){throw 'Observe has no firewall changes to apply.'};if(-not $PSCmdlet.ShouldProcess('Windows Firewall',$Profile)){return}
[IO.Directory]::CreateDirectory($stateRoot)|Out-Null
if(-not $Journal){$Journal=Join-Path $stateRoot ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')}
$runtime=Join-Path $stateRoot 'recovery-runtime';[IO.Directory]::CreateDirectory((Join-Path $runtime 'src'))|Out-Null;[IO.Directory]::CreateDirectory((Join-Path $runtime 'config'))|Out-Null
Copy-Item -LiteralPath $PSCommandPath -Destination (Join-Path $runtime 'Network.ps1') -Force
foreach($name in @('Wind.Core.psm1','Wind.Network.psm1','MsPrivate.IO.psm1')){Copy-Item -LiteralPath (Join-Path $PSScriptRoot ('src\'+$name)) -Destination (Join-Path $runtime ('src\'+$name)) -Force}
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'config\network.json') -Destination (Join-Path $runtime 'config\network.json') -Force
$result=Invoke-WindNetworkApply -Profile $Profile -RollbackMinutes $RollbackMinutes -JournalPath $Journal -RecoveryScript (Join-Path $runtime 'Network.ps1') -Confirm:$false
Write-Output "Network journal: $Journal";$result|Format-List;return
}
if(-not $Journal){throw "$Mode requires -Journal."}
$saved=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json
$save={param($x) Save-WindJournal -Journal $x -Path $Journal}.GetNewClosure()
if($Mode-eq'Confirm'){Confirm-WindNetwork -Journal $saved -Save $save|Format-List}else{Restore-WindNetwork -Journal $saved -Save $save|Format-List}
+73
View File
@@ -0,0 +1,73 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateSet('Plan','Apply')][string]$Mode='Plan',
[ValidateSet('Auto','EstablishedSafe','FreshInstall','Gentle','Medium','Strong','Maximum')][string]$Preset='Auto',
[switch]$AcknowledgeDestructivePreset,
[switch]$Json
)
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$root=$PSScriptRoot
foreach($module in @('Wind.Core.psm1','Wind.Apps.psm1','Wind.Network.psm1','Wind.Hardware.psm1','Wind.Recall.psm1','MsPrivate.Services.psm1','MsPrivate.Accounts.psm1','MsPrivate.Presets.psm1','MsPrivate.Execution.psm1')){Import-Module (Join-Path $root ('src\'+$module)) -Force}
$installContext=Get-MsPrivateInstallContext
if($Preset-eq'Auto'){$Preset=[string]$installContext.RecommendedPreset}
$p=Get-MsPrivatePreset $Preset
$inventory=@(Get-WindAppInventory)
$appPlan=@(Get-WindAppPlan -Inventory $inventory -SkipBundled:(-not$p.RemoveBundled) -KeepOneDrive:(-not$p.RemoveOneDrive) -KeepEdge:(-not$p.RemoveEdge) -RemovePersonal:$p.RemovePersonal -RemoveStore:$p.RemoveStore)
$policyPlan=@(Get-WindPlan -Context (Get-WindContext) -Store (New-WindRegistryStore) -Profile $p.Profile -Updates $p.Updates)
$servicePlan=if($p.Services){@(Get-MsPrivateServicePlan -Profile Strict -Backend (New-MsPrivateServiceBackend))}else{@()}
$accountPlan=if($p.Accounts){@(Get-MsPrivateAccountPlan -Scope AllAppAccounts -Backend (New-MsPrivateAccountBackend))}else{@()}
$networkPlan=Get-WindNetworkPlan -Profile $p.Network -RollbackMinutes ([int]$p.RollbackMinutes)
$sensorPlan=if($p.DisableSensors){@(Get-WindSensorInventory|Where-Object {$_.Status -eq 'OK'})}else{@()}
$recallState=if($p.Recall){Get-WindRecallState}else{$null}
$removeRecallFeature=[bool]($p.RemoveRecall-and$null-ne$recallState-and$recallState.FeaturePresent)
$recallPlan=if($p.Recall){Get-WindRecallPlan -RemoveFeature:$removeRecallFeature}else{$null}
$recallDisposition=Resolve-MsPrivateRecallDisposition -Requested $p.Recall -Plan $recallPlan
$tools=@();if($p.InstallMullvad){$tools+='Mullvad'};if($p.InstallVeraCrypt){$tools+='VeraCrypt'}
$summary=[pscustomobject]@{
Schema=2;Status='Planned';InstallContext=$installContext;Preset=$p;Policies=$policyPlan;Services=$servicePlan;Accounts=$accountPlan;Applications=$appPlan
Network=$networkPlan;Sensors=$sensorPlan;Recall=$recallPlan;Tools=$tools
Skipped=@($recallDisposition.Skipped)
Irreversible='Application/account/Recall/cache deletion is not restored by policy journals. Store removal is current-user scope. Maximum isolation retains a timed recovery until explicitly confirmed.'
}
if($Mode-eq'Plan'){
if($Json){$summary|ConvertTo-Json -Depth 12}else{
$p|Select-Object Id,Label,Description|Format-List
$policyPlan|Select-Object Id,Status,Desired|Format-Table
$appPlan|Where-Object {$_.Status -ne 'Keep'}|Select-Object Name,Status,Scope,Reason|Format-Table -Wrap
$sensorPlan|Select-Object Kind,Name,Status,Risk|Format-Table -Wrap
$networkPlan.Items|Format-Table -Wrap
if($recallPlan){$recallPlan.Items|Format-Table -Wrap};if($summary.Skipped.Count){Write-Warning ('Skipped: '+($summary.Skipped-join'; '))}
Write-Output $summary.Irreversible
}
return
}
if($Json){throw '-Json is available only with -Mode Plan.'}
$destructive=[bool]($p.Services-or$p.Accounts-or$p.RemoveBundled-or$p.RemovePersonal-or$p.RemoveStore-or$p.RemoveOneDrive-or$p.RemoveEdge-or$p.Recall-or$p.Cleanup-or$p.PrivacyCleanup-or$p.DisableSensors-or$p.Network-ne'Observe')
if($destructive-and-not$AcknowledgeDestructivePreset){throw "$Preset requires -AcknowledgeDestructivePreset after reviewing -Mode Plan."}
$policyIssues=@($policyPlan|Where-Object {$_.Status -in @('Conflict','ReadError')})
$appIssues=@($appPlan|Where-Object {$_.Status -eq 'Blocked'})
$networkIssues=if($p.Network-ne'Observe'){@($networkPlan.Items|Where-Object {$_.Status -ne 'WouldChange'})}else{@()}
if(-not$PSCmdlet.ShouldProcess("Preset $Preset","Apply the complete reviewed preset: $(@($appPlan|Where-Object {$_.Status -eq 'Remove'}).Count) app removals, $(@($sensorPlan).Count) sensor disables, network $($p.Network)")){return $summary}
Assert-WindMutationAllowed
$eligible=@($policyPlan|Where-Object {$_.Status -in @('WouldChange','Configured')}|ForEach-Object Id)
$hasAppActions=[bool](@($p.Browsers).Count-or@($appPlan | Where-Object { $_.Status -eq 'Remove' }).Count)
$runState=[pscustomobject]@{NetworkJournal=$null};$groups=[Collections.Generic.List[object]]::new()
if($eligible.Count-or$policyIssues.Count){$groups.Add([pscustomobject]@{Id='Policies';Label='Registry policy writes';DependsOn=@();Action=({if($eligible.Count){& (Join-Path $root 'ms-private.ps1') -Mode Apply -Profile $p.Profile -Updates $p.Updates -Id $eligible -Confirm:$false};if($policyIssues.Count){throw "$($policyIssues.Count) requested policies had a conflict or read error; eligible policies were attempted."}}.GetNewClosure())})}
if($p.Services){$groups.Add([pscustomobject]@{Id='Services';Label='Service configuration';DependsOn=@();Action=({& (Join-Path $root 'Services.ps1') -Mode Apply -Profile Strict -Confirm:$false}.GetNewClosure())})}
if($p.Accounts){$groups.Add([pscustomobject]@{Id='Accounts';Label='Credential and application-account purge';DependsOn=@();Action=({& (Join-Path $root 'Accounts.ps1') -Mode Apply -Scope AllAppAccounts -AcknowledgeAccountDataDeletion -Confirm:$false}.GetNewClosure())})}
if($tools.Count){$groups.Add([pscustomobject]@{Id='PrivacyTools';Label='Privacy-tool installation';DependsOn=@();Action=({& (Join-Path $root 'Install-Apps.ps1') -App $tools -Confirm:$false}.GetNewClosure())})}
if($hasAppActions-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."};& (Join-Path $root 'Manage-Apps.ps1') -Mode Apply -Browser @($p.Browsers) -SkipBundled:(-not$p.RemoveBundled) -KeepOneDrive:(-not$p.RemoveOneDrive) -KeepEdge:(-not$p.RemoveEdge) -RemovePersonal:$p.RemovePersonal -RemoveStore:$p.RemoveStore -ReviewedPlan $appPlan -Confirm:$false}.GetNewClosure())})}
if($p.Network-ne'Observe'){$groups.Add([pscustomobject]@{Id='Network';Label='Firewall changes';DependsOn=@();Action=({if($networkIssues.Count){throw 'Required network-enforcement capabilities are unavailable.'};$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');& (Join-Path $root 'Network.ps1') -Mode Apply -Profile $p.Network -RollbackMinutes ([int]$p.RollbackMinutes) -Journal $runState.NetworkJournal -Confirm:$false}.GetNewClosure())})}
if($sensorPlan.Count){$groups.Add([pscustomobject]@{Id='Sensors';Label='Exact device disablement';DependsOn=@();Action=({& (Join-Path $root 'Hardware.ps1') -Mode Apply -InstanceId @($sensorPlan.InstanceId) -Confirm:$false}.GetNewClosure())})}
if($p.Recall-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.'};& (Join-Path $root 'Recall.ps1') -Mode Apply -AcknowledgeSnapshotDeletion -RemoveFeature:$removeRecallFeature -Confirm:$false}.GetNewClosure())})}
if($p.Cleanup-or$p.PrivacyCleanup){$groups.Add([pscustomobject]@{Id='Cleanup';Label='Allowlisted file cleanup';DependsOn=@();Action=({if($p.Cleanup){& (Join-Path $root 'Cleanup.ps1') -Mode Run -AgeDays 7 -Category Temp -Confirm:$false};if($p.PrivacyCleanup){& (Join-Path $root 'Cleanup.ps1') -Mode Run -AgeDays 0 -Category @('CrashReports','RecentItems','ThumbnailCache','BrowserCache','PowerShellHistory') -ClearClipboard -Confirm:$false}}.GetNewClosure())})}
if($p.CleanupSchedule){$groups.Add([pscustomobject]@{Id='CleanupSchedule';Label='Cleanup schedule';DependsOn=@();Action=({& (Join-Path $root 'Cleanup.ps1') -Mode InstallSchedule -AgeDays 7 -Confirm:$false}.GetNewClosure())})}
if($p.Lock){$groups.Add([pscustomobject]@{Id='LockHandler';Label='Session-lock handler';DependsOn=@();Action=({& (Join-Path $root 'Lock.ps1') -Mode Install -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')
$result=Invoke-MsPrivateGroupPlan -Groups @($groups) -ReportPath $runPath
[pscustomobject]@{Status=$result.Status;Preset=$Preset;Skipped=$summary.Skipped;RunReport=$runPath;Groups=$result.Entries;NetworkJournal=$runState.NetworkJournal;NextStep=if($p.Network-eq'Isolate'-and($result.Entries | Where-Object { $_.Id -eq 'Network' }).Status-eq'Succeeded'){"Verify recovery and connectivity. Confirm or restore the timed isolation journal: $($runState.NetworkJournal)"}else{'Reboot, validate security/update behavior and inspect local journals/reports.'}}
if($result.Status-ne'Completed'){throw 'One or more operation groups failed or had an unmet dependency. Independent groups were still attempted; inspect the run report.'}
+161 -50
View File
@@ -1,79 +1,190 @@
# Wind privacy toolkit # ms-private
A local Windows privacy configuration tool and an evidence-based implementation plan. Browser choices: **Tor Browser, Zen, Helium**. Research checked 2026-09-18. **Make Windows yours.** A local Windows privacy toolkit for documented settings, app removal, browser choice, network controls, exact sensor selection, Recall, cleanup and session-lock actions. The release is one self-contained x64 executable with an elevation manifest and a graphical wizard. Its reviewable PowerShell engine is embedded in the executable; it never downloads and executes changing scripts from the web.
**Status: development prototype.** Audit and planning run without elevation. Policy apply/restore is implemented, with transaction tests; Windows behavior and network effects still require VM testing. It does not certify anonymity or zero Microsoft traffic. ```text
m s - p r i v a t e
## Start here Make Windows yours.
----------------------------------------------------
```powershell > Review plan
./Wind.ps1 Privacy profile Strict
./Wind.ps1 -Mode Plan -Profile Strict Browsers Zen
./Wind.ps1 -Mode Guide Apps & debloat
./Install-Apps.ps1 -App Tor,Zen,Helium,Mullvad -WhatIf Microsoft app accounts Purge
Windows updates Keep
Apply selected changes
Restore policies
Default browser settings
Privacy guide
Quit
----------------------------------------------------
Arrows move Enter select Esc back
``` ```
The default mode is a read-only policy audit. Guide prints Windows Settings links and browser setup guidance, including the Home edition path. Nothing downloads or executes remote scripts. ## Run
On supported Windows 11 Pro/Enterprise/Education editions, from a 64-bit elevated PowerShell: 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.
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.
Source and development runs remain available with 64-bit Windows PowerShell 5.1 or PowerShell 7:
```powershell ```powershell
./Wind.ps1 -Mode Apply -Profile Standard -WhatIf .\ms-private.ps1
./Wind.ps1 -Mode Apply -Profile Standard # or the tabbed graphical wizard
# Optional: notify before downloading updates, preserving the update engine .\ms-private-GUI.ps1
./Wind.ps1 -Mode Apply -Profile Standard -Updates Notify
# Restore using the journal path printed by Apply
./Wind.ps1 -Mode Restore -Journal 'C:\path\to\journal.json' -WhatIf
./Wind.ps1 -Mode Restore -Journal 'C:\path\to\journal.json'
``` ```
Apply is explicit. Standard keeps existing update settings. Updates can be Keep, Automatic, Notify or Manual. Manual disables automatic updates; it does not promise no update network traffic or control browser/Store updates. Re-run Audit after reboot, then follow the behavioral checks in the validation plan. Move with **Up/Down**, select with **Enter**, toggle checkboxes with **Space**, return with **Esc**. **Page Up/Down** scroll long lists. Type to filter applications; Backspace erases the filter. The highlighted item's explanation appears below the list. Choices stay in memory until you apply them.
Windows Home and unreviewed builds are **audit/guide only** for machine policy changes. A registry value being present is not evidence that Windows honors it. The currently reviewed build families are 26100 and 26200; this is a scope gate, not a claim of end-to-end testing. Launching the menu changes nothing. Review the plan, then select **Apply selected changes**. The final confirmation defaults to going back.
## Implemented ## Automatic presets
- Source-linked policy catalog: minimum supported Windows diagnostic level, advertising ID, location, camera, app microphone default, cloud speech, clipboard synchronization, Find My Device, cloud search and Delivery Optimization. Both interfaces use the validated catalog in [presets.json](config/presets.json). At startup, ms-private recommends Fresh Install only when both Windows and the current user profile are no more than seven days old. Older, inconsistent or unreadable timestamps select Established PC. Choosing any preset fills every related control; **Review plan** enumerates the exact policy, application and device targets before one final Apply approval.
- Strict adds clipboard history suppression and DO mode 99 (no peering or DO cloud service; updates still contact their own services).
- Edition/build checks, read errors, app microphone exception detection, and managed-device detection before mutation.
- Exact DWORD value/absence capture, a journal saved before writes, readback, failure rollback, and conflict-aware restoration. No full registry exports, personal identifiers or browsing contents are collected.
- Optional package installation with exact WinGet IDs, normal installer/hash checks, no silent license acceptance, no forced restarts, and `-WhatIf`.
- Tor, Zen and Helium are all available; installing does not select a default or configure the browsers.
## Not implemented yet | Preset | Automatic behavior |
| --- | --- |
| Established PC | Conservative default for an older installation. Applies Standard privacy policy only. Keeps every app, browser, account, service, device, Recall item and file; networking and update settings remain unchanged. |
| Fresh Install | Complete initial setup: Strict policies/services, account purge, Recall removal, reviewed endpoint blocking, cleanup/schedule/lock handling, Mullvad/VeraCrypt, Zen/Tor, detected sensors and every unambiguously removable app including Store, Edge and OneDrive. |
| Gentle | Standard policy and curated promotional-app removal. Keeps personal apps, Store, Edge, OneDrive, accounts, sensors and networking. |
| Medium | Strict policies and services, Microsoft app-account purge, Recall deletion/feature removal, optional-endpoint blocking, cleanup/schedule/lock handling, Mullvad and VeraCrypt installation, and every unambiguously removable Microsoft or third-party app including Store, Edge and OneDrive. |
| Strong | Medium plus Tor Browser and automatic camera, microphone and sensor selection. This can disable Windows Hello and conferencing hardware. |
| Maximum | Strong plus Zen, Helium and Tor and timed default-outbound isolation. Keeping isolation blocks ordinary connectivity, including online updates, until suitable allow rules are created or isolation is restored. |
Network enforcement, packet-capture validation, a graphical wizard, AME packaging, Home GUI automation, debloating, sensor hardware disablement, Recall removal, automatic cleanup, and lock-event handling are specified in [the implementation plan](docs/PLAN.md). Their status is explicit because an incomplete privacy control must not appear enabled. Fresh Install, Medium and stronger presets use exact installed identities. They do not issue wildcard package deletion commands. WebView2, drivers, firmware, runtimes, servicing components, Defender/security products, authentication components, chosen browsers, Mullvad, VeraCrypt and recovery infrastructure stay protected. Store removal is current-user scope and can require an official recovery or Windows repair path to reinstall.
Read [the research and threat model](docs/RESEARCH.md), [Home setup guide](docs/HOME-GUIDE.md), and [validation protocol](docs/VALIDATION.md). ### Source ZIP / repeated security prompts
The [privacy register](outputs/01a0b6c2-22ad-7b62-9a5a-4a09014a9b42/Windows-privacy-register.xlsx) records 85 controls and decisions, 19 tool/research entries, sources, application status and 24 remaining tasks. The [blog draft](outputs/01a0b6c2-22ad-7b62-9a5a-4a09014a9b42/Windows-privacy-blog-draft.md) explains the motivation and evidence. The [supplied GDID script review](docs/GDID-SCRIPT-REVIEW.md) records 12 findings and the two pinned research revisions. None of these artifacts represents a deployed identifier block. Review the download first. In the ZIP's Properties, select **Unblock** before extracting. If already extracted, run the following **from this reviewed project folder**:
## Offline update preparation
Direct downloads require outbound requests. For no direct Microsoft contact from the protected PC, use a separate collector and an isolated Windows environment; see [the offline update design](docs/OFFLINE-UPDATES.md). Cloud protection is unavailable while isolated.
```powershell ```powershell
# Local version/edition/architecture only; no identifying hardware/account fields. Get-ChildItem -LiteralPath . -Recurse -File | Where-Object { $_.Extension -in '.ps1', '.psm1' } | Unblock-File; Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force; .\ms-private-GUI.ps1
./Offline-Updates.ps1 -Json
# Check an explicitly prepared local bundle. Does not download or install.
./Offline-Updates.ps1 -Mode Verify -Manifest 'D:\Updates\bundle.json' -Json
``` ```
Populate `config/offline-bundle.example.json` with reviewed packages and actual hashes first. The verifier checks integrity against an unsigned manifest, target build/architecture and declared prerequisite order. It does not establish publisher authenticity, complete prerequisites, applicability, coverage or isolation. Run `./tests/Run-OfflineUpdateTests.ps1` for its synthetic tests. That line unblocks PowerShell files inside the current reviewed directory, changes policy only for the current PowerShell process, and starts the GUI. It does not fetch code. You do not need `Unrestricted`, a permanent execution-policy change or a download-and-execute command. Organization policies may override process policy; ms-private does not bypass those policies. Releases are currently unsigned. See Microsoft's [Unblock-File](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/unblock-file) and [execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies) documentation.
## Recovery and boundaries ## Defaults
Journals go into `%LOCALAPPDATA%\WindPrivacy\journals` by default and contain only the selected policy values, build/edition and a local random installation marker. Keep them private and restore on the originating Windows installation. No journal is uploaded. Apply/restore use a shared mutex to prevent overlapping Wind transactions. Domain/MDM detection is best effort; do not use on an organization-managed device. | Choice | Default / behavior |
| --- | --- |
| Preset | Automatically Fresh Install only when Windows and the current profile are at most seven days old; otherwise Established PC. Manual selection remains available. |
| Privacy | Standard in Established PC and Gentle; Strict in Fresh Install, Medium, Strong and Maximum. |
| Microsoft app accounts | Kept in Established PC and Gentle; purged in Fresh Install, Medium and stronger. |
| Updates | Keep existing settings. Automatic, Notify and Manual are selectable. No update services are removed. |
| Browsers | Zen selected; choose Zen, Helium, Tor Browser or any combination. All selected browsers are checked before removals begin. |
| Bundled apps | Curated promotional/consumer packages selected for removal; review [the exact catalog](config/debloat.json). |
| Personal apps | Kept in Established PC and Gentle; every unambiguously removable app is selected in Fresh Install, Medium and stronger. |
| OneDrive | Kept in Established PC and Gentle and selected in Fresh Install, Medium and stronger. Cloud-only files remain online; ms-private does not delete sync folders. |
| Edge | Kept in Established PC and Gentle and selected in Fresh Install, Medium and stronger after replacement-browser verification. Windows may refuse removal. WebView2 is preserved. |
Restore changes only catalogued values whose current value still matches what Wind wrote. It reports conflicts instead of overwriting subsequent changes. Repeated restore is safe. Empty registry keys created by Apply may remain. Restoring policy settings cannot recover data already deleted by Windows or third-party programs. App identity cannot reliably distinguish a preinstalled Spotify/Outlook package from the same app installed manually. Uncheck any catalogued apps you want to retain. Unknown OEM apps are opt-in. ms-private protects OS-marked components and known drivers, runtimes, security tools and dependencies; the name-based list cannot identify every third-party dependency.
The core does not change Defender, firewall, encryption, Secure Boot, UAC, update services, licensing, device identifiers, logs, browser data or user files. No hosts blocklist, RAM cleaner, registry cleaner, `irm | iex`, or security exclusions are used. Appx removal affects the current user. Provisioned packages and other users' Appx registrations are left alone. Desktop uninstallers follow their displayed user/machine registration scope. Uninstallers can remove app settings and data: policy recovery journals are **not app backups**.
## Tests Windows restrictions on Edge are respected. There is no forced system-file removal or region-policy editing. If removal is refused, the run stops with an incomplete report; keep Edge in the next plan to continue. Set the replacement default in Windows Settings.
## Command line
```powershell ```powershell
./tests/Run-Tests.ps1 # Policy-only audit, preview, apply and recovery
.\ms-private.ps1 -Mode Audit -Profile Strict -Json
.\ms-private.ps1 -Mode Plan -Profile Strict -Updates Notify
.\ms-private.ps1 -Mode Apply -Profile Strict -WhatIf
.\ms-private.ps1 -Mode Apply -Profile Strict
.\ms-private.ps1 -Mode Restore -Journal 'C:\path\to\saved.journal.json'
# Strict service controls: Connected Devices/CDP, cloud sync/backup, diagnostics, location and WER
.\Services.ps1 -Mode Plan -Profile Strict
.\Services.ps1 -Mode Apply -Profile Strict
.\Services.ps1 -Mode Restore -Journal 'C:\path\to\service-journal.json'
# Current-user Microsoft app-account audit and irreversible sign-out
.\Accounts.ps1 -Mode Plan -Scope AllAppAccounts
.\Accounts.ps1 -Mode Apply -Scope AllAppAccounts -AcknowledgeAccountDataDeletion -WhatIf
.\Accounts.ps1 -Mode Apply -Scope AllAppAccounts -AcknowledgeAccountDataDeletion
# App plan, available IDs, and an explicit apply
.\Manage-Apps.ps1 -Mode Plan -Browser Zen,Helium,Tor
.\Manage-Apps.ps1 -Mode List -Json
.\Manage-Apps.ps1 -Mode Apply -Browser Zen,Helium -WhatIf
.\Manage-Apps.ps1 -Mode Apply -Browser Zen,Helium
# Keep groups or individual apps; use exact IDs returned by List
.\Manage-Apps.ps1 -SkipBundled -KeepOneDrive -KeepEdge
.\Manage-Apps.ps1 -ExtraId 'exact-listed-id' -ExcludeId 'another-listed-id'
# Aggressive exact-inventory preview used by Fresh Install, Medium and stronger presets
.\Manage-Apps.ps1 -Mode Plan -RemovePersonal -RemoveStore -Browser Zen
# Plan or apply one complete preset from the command line
.\Preset.ps1 -Mode Plan -Preset Auto
.\Preset.ps1 -Mode Plan -Preset FreshInstall
.\Preset.ps1 -Mode Apply -Preset FreshInstall -AcknowledgeDestructivePreset
# Optional VPN / encryption software; installation does not configure it
.\Install-Apps.ps1 -App Mullvad,VeraCrypt -WhatIf
# Narrow network preview, timed isolation and local capture
.\Network.ps1 -Mode Plan -Profile BlockOptionalTelemetry
.\Network.ps1 -Mode Apply -Profile Isolate -RollbackMinutes 10
.\Network.ps1 -Mode Validate -Json
.\Network.ps1 -Mode Confirm -Journal 'C:\path\to\network.json'
.\Network.ps1 -Mode CaptureStart
# Exact hardware, Recall, safe cleanup and session-lock handling
.\Hardware.ps1 -Mode List
.\Hardware.ps1 -Mode Plan -InstanceId 'exact PnP instance id'
.\Recall.ps1 -Mode Plan -RemoveFeature
.\Cleanup.ps1 -Mode Plan -AgeDays 7 -Category Temp
.\Cleanup.ps1 -Mode Plan -AgeDays 0 -Category CrashReports,RecentItems,ThumbnailCache,BrowserCache,PowerShellHistory
.\Security.ps1 -Json
.\Lock.ps1 -Mode Install -VeraCryptVolume X:
``` ```
The tests run the transaction engine against an isolated memory store. They do not change Windows policies. The read-only audit can separately be run on the host. Passing these tests is not a substitute for VM and network validation. WinGet must be installed and its `winget` source usable. ms-private retains normal installer prompts, signature/hash checks and license requirements. It does not silently accept source agreements. If WinGet requires source initialization, open `winget list --source winget` yourself, review its prompts and retry. WinGet and installers make network requests and may keep their own logs.
## 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.
- 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.
- Policy changes save original values before writing and roll back failures. Restoration preserves later conflicting edits. Restore newer journals before older ones.
- App operations save intent and per-app outcomes. They stop on ambiguous state, errors, reboot-required results or unsuccessful readback. Earlier successful removals remain removed.
- Account purge is irreversible. It records only catalog IDs and outcomes, never account names, emails, credential targets or secrets. It does not remove the current Windows logon identity or an existing organizational registration.
- Complete presets execute each selected mutation area as a recorded group. A failed group is reported and does not prevent unrelated groups from running; only groups with a failed prerequisite are skipped. Each run writes a summary below `%LOCALAPPDATA%\MsPrivate\runs`, while component recovery journals remain authoritative for restoration.
- Reports and journals stay under `%LOCALAPPDATA%\MsPrivate`. ms-private has no analytics or automatic report uploads. Do not publish local reports: installed-app information can be personal.
Switching Strict to Standard permits clipboard history again and changes DO mode back to 0. It does not restore deleted history. `Updates Keep` retains the current setting, including changes made by an earlier run.
## Advanced controls
`Network.ps1` has three distinct modes. **Observe** changes nothing. **BlockOptionalTelemetry** creates dynamic-FQDN outbound block rules only for the reviewed catalog in [network.json](config/network.json). **Isolate** sets the default outbound action to Block for all firewall profiles and schedules a local SYSTEM rollback before changing them. Confirm an isolation journal only after local recovery and required connectivity checks succeed. Existing explicit allow rules can still pass traffic, and a firewall configuration is not proof of zero telemetry.
Packet captures use the built-in Pktmon tool, remain under `%LOCALAPPDATA%\MsPrivate\network`, and are converted to PCAPNG only when stopped explicitly. Captures may contain private addresses and DNS names. ms-private does not install a TLS interception certificate.
Recall apply requires `-AcknowledgeSnapshotDeletion`. The supported policy deletes existing snapshots; restoring the prior setting cannot recover them. Hardware controls act only on exact selected PnP instance IDs and select nothing by default.
Cleanup uses named, allowlisted categories. It can remove old Temp files, WER queues and crash dumps, Recent Items and Jump Lists, unlocked thumbnail caches, Zen/Tor/Helium cache subdirectories, PowerShell interactive history, selected expired ms-private reports, and the current clipboard. Browser-cache cleanup retains cookies, passwords, bookmarks, sessions and settings. It skips reparse points, changed files and locked files. It excludes Documents, Downloads, non-cache browser profile data, credential stores, ms-private recovery journals, Windows event logs, Defender logs, update logs and authentication logs. Deletion reduces local retention but does not erase provider records or guarantee physical erasure on SSDs.
The lock handler clears the current clipboard and can gracefully unmount selected VeraCrypt drive letters; it does not claim to wipe RAM. `Security.ps1` audits BitLocker/device-encryption status, Defender and cloud-protection posture, firewall profiles, Secure Boot, TPM, VBS/Credential Guard, LSA protection, UAC, SMB/SMB1, remote access and protected update/security services without reading recovery keys, account names, Defender exclusions or secrets.
Windows Home uses `Home.ps1` and the wizard's Home tab to open supported Settings pages. ms-private does not automate localized clicks or claim a visible toggle changed without user verification.
## Privacy boundaries
ms-private now applies 56 reviewed privacy policies, optionally disables eight privacy-related service templates, can purge current-user Microsoft application sign-ins, cleans the named privacy-bearing artifacts above and audits encryption and security status. Strict is the default in both wizards. The expanded policy catalog covers user-scoped personalization and input collection, Widgets, search highlights/location, device metadata, downloadable fonts, MSRT reporting, LLMNR and additional app access to foreground text, screen capture, presence, gaze, motion, notifications, phones, radios, location and background execution. It blocks future consumer Microsoft authentication in apps and new workplace registration while preserving the Windows logon identity. It protects Windows Security, Defender, firewall, authentication and update event channels because those records are evidence used to detect and investigate compromise; deleting them does not prevent collection or transmission. ms-private does not promise anonymity or zero Microsoft traffic, and it does not implement RAM/registry cleaners, undocumented identifier rotation, hosts-file blocklists, TLS interception or blanket Microsoft/Azure IP blocking. Network collection is reduced through supported policy, a reviewed 19-endpoint firewall catalog, service shutdown and capture-based validation with recovery paths. Catalog validation rejects update, trust, Defender, SmartScreen, Store-revocation and WNS endpoint families.
Read the [complete tool breakdown](docs/TOOL-BREAKDOWN.md), [current roadmap](docs/ROADMAP.md), [native API audit](docs/NATIVE-API.md), [privacy notes](docs/PRIVACY.md), [anonymity operating guide](docs/ANONYMITY.md), [blog draft](docs/BLOG.md), [privacy register](docs/Windows-privacy-register.xlsx), [manual guide](docs/HOME-GUIDE.md) and [offline update design](docs/OFFLINE-UPDATES.md). `Offline-Updates.ps1` inventories the OS or verifies a prepared bundle's integrity; it does not download, authenticate or install updates.
## Development and release status
```powershell
.\tests\Run-All.ps1
.\Build-Executable.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).
`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.
+8
View File
@@ -0,0 +1,8 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('Audit','Plan','Apply','Restore')][string]$Mode='Plan',[switch]$RemoveFeature,[switch]$AcknowledgeSnapshotDeletion,[string]$Journal,[switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/Wind.Recall.psm1')
if($Mode -in @('Audit','Plan')){$x=Get-WindRecallPlan -RemoveFeature:$RemoveFeature;if($Json){$x|ConvertTo-Json -Depth 8}else{$x.Items|Format-Table -Wrap};return}
Assert-WindMutationAllowed;$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\recall';[IO.Directory]::CreateDirectory($root)|Out-Null
if($Mode -eq 'Apply'){if(-not $Journal){$Journal=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')};Invoke-WindRecallApply -RemoveFeature:$RemoveFeature -AcknowledgeSnapshotDeletion:$AcknowledgeSnapshotDeletion -JournalPath $Journal -Confirm:$false;Write-Output "Recall journal: $Journal";return}
if(-not $Journal){throw 'Restore requires -Journal.'};$j=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$save={param($x)Save-WindJournal -Journal $x -Path $Journal}.GetNewClosure();Restore-WindRecall -Journal $j -Save $save -Confirm:$false
+9
View File
@@ -0,0 +1,9 @@
# Security
ms-private runs locally and can make administrator-level changes. Review the source and the concrete plan before applying. Never upload recovery journals, identifiers, tokens, browsing histories or unredacted installed-app inventories to a public issue.
The Windows release is self-contained and requests elevation with an embedded `requireAdministrator` manifest. Before launch, compare its SHA-256 with the adjacent checksum over a trusted channel. The executable is not code-signed yet. At runtime it verifies the embedded payload, installs it under `%ProgramFiles%\ms-private\runtime`, and refuses or repairs hash, path, reparse-point and undeclared-file failures before starting the wizard.
Report reproducible bugs with the Windows edition/build, PowerShell version, ms-private revision and a minimal redacted error. For a security issue, use the repository owner's private reporting channel when available; do not publish an exploit or personal report by default.
App reports are an audit trail, not a backup or rollback mechanism. Use trusted original installers to recover removed apps. Keep policy journals local and restore them on the originating installation.
+4
View File
@@ -0,0 +1,4 @@
#Requires -Version 5.1
[CmdletBinding()]param([switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Security.psm1')
$status=Get-WindSecurityStatus;if($Json){$status|ConvertTo-Json -Depth 7}else{$status|Format-List;Write-Output 'BitLocker / device encryption:';$status.BitLocker|Format-Table -Wrap;Write-Output 'Firewall:';$status.Firewall|Format-Table -Wrap}
+11
View File
@@ -0,0 +1,11 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]param([ValidateSet('Audit','Plan','Apply','Restore')][string]$Mode='Plan',[ValidateSet('Standard','Strict')][string]$Profile='Strict',[string]$Journal,[switch]$Json)
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1');Import-Module (Join-Path $PSScriptRoot 'src/MsPrivate.Services.psm1') -Force
$backend=New-MsPrivateServiceBackend
if($Mode-in@('Audit','Plan')){$plan=@(Get-MsPrivateServicePlan -Profile $Profile -Backend $backend);if($Json){[pscustomobject]@{Schema=1;Profile=$Profile;Claim='Local service state only; GDID suppression requires network capture validation.';Services=$plan}|ConvertTo-Json -Depth 8}else{$plan|Select-Object Id,Name,Status,Impact|Format-Table -Wrap};return}
Assert-WindMutationAllowed
$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\services';[IO.Directory]::CreateDirectory($root)|Out-Null
if($Mode-eq'Apply'){$plan=@(Get-MsPrivateServicePlan -Profile $Profile -Backend $backend);$Journal=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json')}elseif([string]::IsNullOrWhiteSpace($Journal)){throw 'Restore requires -Journal.'}
if(-not$PSCmdlet.ShouldProcess('Selected Windows privacy services',$Mode)){return}
if($Mode-eq'Apply'){$journal=$null;$save={param($x)Save-MsPrivateServiceJournal -Journal $x -Path $Journal}.GetNewClosure();$journal=Invoke-MsPrivateServiceApply -Plan $plan -Backend $backend -Save $save}else{$journal=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$save={param($x)Save-MsPrivateServiceJournal -Journal $x -Path $Journal}.GetNewClosure();$journal=Restore-MsPrivateServices -Journal $journal -Backend $backend -Save $save}
$journal|Select-Object Kind,Status,CreatedUtc|Format-List;Write-Output "Service recovery journal: $Journal"
+5
View File
@@ -0,0 +1,5 @@
# ms-private AME wrapper
This wrapper packages the same ms-private scripts and launches `ms-private-GUI.ps1`. It does not carry separate privacy tweaks, disable Defender, remove Windows Update, or hide unsupported controls.
Build with `Build-AmePlaybook.ps1`. The builder requires 7-Zip because AME playbooks use an encrypted `.apbx` archive. Review and VM-test the resulting package with the validation protocol before distribution.
+10
View File
@@ -0,0 +1,10 @@
---
title: ms-private
description: Launch the reviewable ms-private wizard
privilege: Admin
actions:
- !run:
exeDir: true
exe: "ms-private\\Launch.cmd"
wait: true
runas: currentUser
@@ -0,0 +1,4 @@
@echo off
setlocal
powershell.exe -NoProfile -STA -ExecutionPolicy RemoteSigned -File "%~dp0ms-private-GUI.ps1"
exit /b %errorlevel%
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Playbook>
<Name>ms-private</Name>
<Username>Windows</Username>
<ShortDescription>Reviewable Windows privacy controls with recovery journals</ShortDescription>
<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>
<Details>Every selected control is previewed and applied by the same PowerShell engine used by the standalone package.</Details>
<Version>0.0.1</Version>
<UniqueId>64d76b08-f58d-499c-aadc-1d72d620e704</UniqueId>
<Overhaul>false</Overhaul>
<SupportedBuilds>
<string>19045</string>
<string>22631</string>
<string>26100</string>
<string>26200</string>
</SupportedBuilds>
<Requirements>
<Requirement>AdministratorPasswordSet</Requirement>
<Requirement>PluggedIn</Requirement>
</Requirements>
<ProgressText>Preparing ms-private.</ProgressText>
<Website>https://git.symonch.uk/0x000/ms-private</Website>
<Git>https://git.symonch.uk/0x000/ms-private</Git>
</Playbook>
+21
View File
@@ -0,0 +1,21 @@
{
"Schema": 1,
"Packages": [
{"Id":"store","Name":"Microsoft.WindowsStore","Label":"Microsoft Store","Scope":"Consumer","Impact":"Deletes current-user Store app data and sign-in details while leaving the Store installed for app updates and revocation."},
{"Id":"xbox-identity","Name":"Microsoft.XboxIdentityProvider","Label":"Xbox Identity Provider","Scope":"Consumer","Impact":"Deletes current-user Xbox identity-provider data and sign-in details."},
{"Id":"xbox-app","Name":"Microsoft.XboxApp","Label":"Xbox app","Scope":"Consumer","Impact":"Deletes current-user Xbox app data and sign-in details if the package remains installed."},
{"Id":"gaming-app","Name":"Microsoft.GamingApp","Label":"Xbox Gaming app","Scope":"Consumer","Impact":"Deletes current-user Gaming app data and sign-in details if the package remains installed."},
{"Id":"gaming-overlay","Name":"Microsoft.XboxGamingOverlay","Label":"Xbox Game Bar","Scope":"Consumer","Impact":"Deletes current-user Game Bar data and sign-in details if the package remains installed."},
{"Id":"office-hub","Name":"Microsoft.MicrosoftOfficeHub","Label":"Microsoft 365 hub","Scope":"Consumer","Impact":"Deletes current-user Microsoft 365 hub data and sign-in details; desktop Office activation is separate."},
{"Id":"teams-personal","Name":"MSTeams","Label":"Microsoft Teams","Scope":"Consumer","Impact":"Deletes current-user packaged Teams data and sign-in details if installed."},
{"Id":"web-account-broker","Name":"Microsoft.AAD.BrokerPlugin","Label":"Windows Web Account Manager broker","Scope":"AllAppAccounts","Impact":"Deletes shared current-user web-account broker data and signs applications out. Windows and apps can request authentication again unless policy blocks it."},
{"Id":"accounts-control","Name":"Microsoft.AccountsControl","Label":"Windows account controls","Scope":"AllAppAccounts","Impact":"Resets current-user Windows account-control app data. Device logon and organizational registration remain separate."}
],
"CredentialTargetPatterns": [
"(?i)^MicrosoftAccount:",
"(?i)^XboxLive:",
"(?i)^Xbl",
"(?i)^SSO_POP_",
"(?i)(OneDrive|MicrosoftOffice|msteams|login\\.live\\.com|virtualapp/didlogical)"
]
}
+120
View File
@@ -0,0 +1,120 @@
{
"Schema": 1,
"RuleGroup": "ms-private",
"OptionalTelemetry": [
{
"Id": "telemetry-vortex",
"Fqdn": "vortex-win.data.microsoft.com",
"Purpose": "Windows diagnostic data endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-settings",
"Fqdn": "settings-win.data.microsoft.com",
"Purpose": "Windows settings and diagnostic configuration endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-watson",
"Fqdn": "watson.telemetry.microsoft.com",
"Purpose": "Windows error reporting endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-functional-events",
"Fqdn": "functional.events.data.microsoft.com",
"Purpose": "Windows diagnostic data endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-self-events",
"Fqdn": "self.events.data.microsoft.com",
"Purpose": "Connected User Experiences and Telemetry endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-v10-events",
"Fqdn": "v10.events.data.microsoft.com",
"Purpose": "Connected User Experiences and Telemetry endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-browser-events",
"Fqdn": "browser.events.data.msn.com",
"Purpose": "Windows diagnostic data endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-command",
"Fqdn": "telecommand.telemetry.microsoft.com",
"Purpose": "Windows Error Reporting endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-command-service",
"Fqdn": "www.telecommandsvc.microsoft.com",
"Purpose": "Windows Error Reporting endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "telemetry-settings-data",
"Fqdn": "settings.data.microsoft.com",
"Purpose": "Windows dynamic settings endpoint",
"Source": "https://learn.microsoft.com/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "location-inference",
"Fqdn": "inference.location.live.net",
"Purpose": "Windows location inference endpoint; paired with the location policies",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "device-metadata-service",
"Fqdn": "dmd.metaservices.microsoft.com",
"Purpose": "Device metadata download endpoint; blocked only in the optional telemetry profile",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "font-streaming-service",
"Fqdn": "fs.microsoft.com",
"Purpose": "Downloadable font service endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "widgets-cdn",
"Fqdn": "widgetcdn.azureedge.net",
"Purpose": "Windows Widgets content delivery endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "widgets-service",
"Fqdn": "widgetservice.azurefd.net",
"Purpose": "Windows Widgets service endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "spotlight-arc",
"Fqdn": "arc.msn.com",
"Purpose": "Windows Spotlight and personalized content endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "spotlight-iris",
"Fqdn": "ris.api.iris.microsoft.com",
"Purpose": "Windows Spotlight recommendation endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "spotlight-feed",
"Fqdn": "fd.api.iris.microsoft.com",
"Purpose": "Windows Spotlight feed endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "spotlight-static-view",
"Fqdn": "staticview.msn.com",
"Purpose": "Windows Spotlight static-content endpoint",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
}
]
}
+520
View File
@@ -74,6 +74,210 @@
"Impact": "Cloud speech functionality is unavailable. Does not govern every application's speech service.", "Impact": "Cloud speech functionality is unavailable. Does not govern every application's speech service.",
"Exceptions": [] "Exceptions": []
}, },
{
"Id": "activity-feed",
"Title": "Disable Windows Activity Feed and cloud roaming",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\System",
"Name": "EnableActivityFeed",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#enableactivityfeed",
"Impact": "Prevents Windows activities from being published and disables Activity Feed cloud sync.",
"Exceptions": []
},
{
"Id": "publish-user-activities",
"Title": "Disallow publishing user activities",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\System",
"Name": "PublishUserActivities",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#publishuseractivities",
"Impact": "Stops applications and Windows from publishing supported User Activity records.",
"Exceptions": []
},
{
"Id": "upload-user-activities",
"Title": "Disallow uploading user activities",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\System",
"Name": "UploadUserActivities",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#uploaduseractivities",
"Impact": "Prevents supported published User Activity records from being uploaded.",
"Exceptions": []
},
{
"Id": "app-account-info",
"Title": "Deny Windows apps account-information access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessAccountInfo",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessaccountinfo",
"Impact": "Strict denies Windows apps access to account name, picture and related account information. Desktop applications are separate.",
"Exceptions": ["LetAppsAccessAccountInfo_ForceAllowTheseApps", "LetAppsAccessAccountInfo_UserInControlOfTheseApps"]
},
{
"Id": "app-calendar",
"Title": "Deny Windows apps calendar access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessCalendar",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesscalendar",
"Impact": "Strict blocks Windows apps from reading calendars; calendar-dependent apps can fail.",
"Exceptions": ["LetAppsAccessCalendar_ForceAllowTheseApps", "LetAppsAccessCalendar_UserInControlOfTheseApps"]
},
{
"Id": "app-contacts",
"Title": "Deny Windows apps contacts access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessContacts",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesscontacts",
"Impact": "Strict blocks Windows apps from reading contacts; contacts-dependent apps can fail.",
"Exceptions": ["LetAppsAccessContacts_ForceAllowTheseApps", "LetAppsAccessContacts_UserInControlOfTheseApps"]
},
{
"Id": "app-call-history",
"Title": "Deny Windows apps call-history access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessCallHistory",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesscallhistory",
"Impact": "Strict blocks Windows apps from reading call history.",
"Exceptions": ["LetAppsAccessCallHistory_ForceAllowTheseApps", "LetAppsAccessCallHistory_UserInControlOfTheseApps"]
},
{
"Id": "app-email",
"Title": "Deny Windows apps email access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessEmail",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessemail",
"Impact": "Strict blocks Windows apps from reading or sending email through the Windows capability.",
"Exceptions": ["LetAppsAccessEmail_ForceAllowTheseApps", "LetAppsAccessEmail_UserInControlOfTheseApps"]
},
{
"Id": "app-messaging",
"Title": "Deny Windows apps messaging access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessMessaging",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessmessaging",
"Impact": "Strict blocks Windows apps from reading or sending messages through the Windows capability.",
"Exceptions": ["LetAppsAccessMessaging_ForceAllowTheseApps", "LetAppsAccessMessaging_UserInControlOfTheseApps"]
},
{
"Id": "app-diagnostics",
"Title": "Deny Windows apps diagnostic information",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsGetDiagnosticInfo",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsgetdiagnosticinfo",
"Impact": "Strict blocks Windows apps from getting diagnostic information about other apps, including user name.",
"Exceptions": ["LetAppsGetDiagnosticInfo_ForceAllowTheseApps", "LetAppsGetDiagnosticInfo_UserInControlOfTheseApps"]
},
{
"Id": "app-trusted-devices",
"Title": "Deny Windows apps trusted-device access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessTrustedDevices",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesstrusteddevices",
"Impact": "Strict blocks Windows apps from accessing trusted devices; device companion features can fail.",
"Exceptions": ["LetAppsAccessTrustedDevices_ForceAllowTheseApps", "LetAppsAccessTrustedDevices_UserInControlOfTheseApps"]
},
{
"Id": "app-unpaired-devices",
"Title": "Deny Windows apps communication with unpaired devices",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsSyncWithDevices",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappssyncwithdevices",
"Impact": "Strict blocks Windows apps from communicating with unpaired wireless devices.",
"Exceptions": ["LetAppsSyncWithDevices_ForceAllowTheseApps", "LetAppsSyncWithDevices_UserInControlOfTheseApps"]
},
{
"Id": "app-voice-activation",
"Title": "Deny Windows app voice activation",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsActivateWithVoice",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsactivatewithvoice",
"Impact": "Strict blocks Windows apps from activation by voice.",
"Exceptions": ["LetAppsActivateWithVoice_ForceAllowTheseApps", "LetAppsActivateWithVoice_UserInControlOfTheseApps"]
},
{
"Id": "app-voice-above-lock",
"Title": "Deny Windows app voice activation while locked",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsActivateWithVoiceAboveLock",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsactivatewithvoiceabovelock",
"Impact": "Strict blocks Windows apps from voice activation while the session is locked.",
"Exceptions": ["LetAppsActivateWithVoiceAboveLock_ForceAllowTheseApps", "LetAppsActivateWithVoiceAboveLock_UserInControlOfTheseApps"]
},
{
"Id": "feedback-prompts",
"Title": "Disable Microsoft feedback prompts",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\DataCollection",
"Name": "DoNotShowFeedbackNotifications",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-experience#donotshowfeedbacknotifications",
"Impact": "Stops Windows Feedback prompts. This setting does not by itself change diagnostic-data transmission.",
"Exceptions": []
},
{
"Id": "windows-error-reporting",
"Title": "Disable Windows Error Reporting transmission",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\Windows Error Reporting",
"Name": "Disabled",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "wer-additional-data",
"Title": "Prevent Windows Error Reporting second-level data",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\Windows Error Reporting",
"Name": "DontSendAdditionalData",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{ {
"Id": "clipboard-sync", "Id": "clipboard-sync",
"Title": "Disallow cross-device clipboard synchronization", "Title": "Disallow cross-device clipboard synchronization",
@@ -122,6 +326,286 @@
"Impact": "Stops searching cloud sources such as OneDrive and SharePoint. Does not establish that all Start web queries are disabled.", "Impact": "Stops searching cloud sources such as OneDrive and SharePoint. Does not establish that all Start web queries are disabled.",
"Exceptions": [] "Exceptions": []
}, },
{
"Id": "tailored-experiences",
"Title": "Disable tailored experiences from diagnostic data",
"Hive": "CurrentUser",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\CloudContent",
"Name": "DisableTailoredExperiencesWithDiagnosticData",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-experience#allowtailoredexperienceswithdiagnosticdata",
"Impact": "Stops Windows from using diagnostic data to personalize tips, advertisements and recommendations for the current user.",
"Exceptions": []
},
{
"Id": "implicit-text-collection",
"Title": "Restrict implicit typing collection",
"Hive": "CurrentUser",
"Path": "SOFTWARE\\Microsoft\\InputPersonalization",
"Name": "RestrictImplicitTextCollection",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "implicit-ink-collection",
"Title": "Restrict implicit inking collection",
"Hive": "CurrentUser",
"Path": "SOFTWARE\\Microsoft\\InputPersonalization",
"Name": "RestrictImplicitInkCollection",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "search-location",
"Title": "Prevent Windows Search from using location",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\Windows Search",
"Name": "AllowSearchToUseLocation",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#allowsearchtouselocation",
"Impact": "Search results will not use device location for local relevance.",
"Exceptions": []
},
{
"Id": "search-highlights",
"Title": "Disable dynamic search highlights",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\Windows Search",
"Name": "EnableDynamicContentInWSB",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#enabledynamiccontentinwsb",
"Impact": "Removes rotating web content and highlights from the Windows search box.",
"Exceptions": []
},
{
"Id": "widgets",
"Title": "Disable Windows Widgets",
"Path": "SOFTWARE\\Policies\\Microsoft\\Dsh",
"Name": "AllowNewsAndInterests",
"Standard": 0,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-newsandinterests#allownewsandinterests",
"Impact": "Disables the Widgets experience and its news, weather and personalized-content requests.",
"Exceptions": []
},
{
"Id": "device-metadata",
"Title": "Disable device metadata downloads",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\Device Metadata",
"Name": "PreventDeviceMetadataFromNetwork",
"Standard": 0,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "font-streaming",
"Title": "Disable downloadable fonts",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\System",
"Name": "EnableFontProviders",
"Standard": 1,
"Strict": 0,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "msrt-reporting",
"Title": "Disable Malicious Software Removal Tool infection reports",
"Path": "SOFTWARE\\Policies\\Microsoft\\MRT",
"Name": "DontReportInfectionInformation",
"Standard": 1,
"Strict": 1,
"EnterpriseValue": null,
"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.",
"Exceptions": []
},
{
"Id": "llmnr",
"Title": "Disable multicast name resolution",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient",
"Name": "EnableMulticast",
"Standard": 1,
"Strict": 0,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-dnsclient#pol-llmnr",
"Impact": "Strict disables LLMNR and reduces local-network name-resolution spoofing exposure. Legacy printers or peer discovery that depend on LLMNR can stop resolving by name.",
"Exceptions": []
},
{
"Id": "app-location",
"Title": "Deny Windows apps location access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessLocation",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesslocation",
"Impact": "Strict denies covered Windows apps access to Windows location; IP and network-based inference remain possible.",
"Exceptions": ["LetAppsAccessLocation_ForceAllowTheseApps", "LetAppsAccessLocation_UserInControlOfTheseApps"]
},
{
"Id": "app-foreground-text",
"Title": "Deny Windows apps foreground-window text access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessForegroundText",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessforegroundtext",
"Impact": "Strict denies covered apps access to text in the active window; accessibility and assistive workflows can be affected.",
"Exceptions": ["LetAppsAccessForegroundText_ForceAllowTheseApps", "LetAppsAccessForegroundText_UserInControlOfTheseApps"]
},
{
"Id": "app-gaze-input",
"Title": "Deny Windows apps gaze input",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessGazeInput",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessgazeinput",
"Impact": "Strict denies covered apps access to eye-tracking input; gaze-based accessibility can stop working.",
"Exceptions": ["LetAppsAccessGazeInput_ForceAllowTheseApps", "LetAppsAccessGazeInput_UserInControlOfTheseApps"]
},
{
"Id": "app-screen-capture",
"Title": "Deny programmatic screen capture by Windows apps",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessGraphicsCaptureProgrammatic",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessgraphicscaptureprogrammatic",
"Impact": "Strict denies covered apps programmatic screen capture; conferencing, recording and support tools can fail.",
"Exceptions": ["LetAppsAccessGraphicsCaptureProgrammatic_ForceAllowTheseApps", "LetAppsAccessGraphicsCaptureProgrammatic_UserInControlOfTheseApps"]
},
{
"Id": "app-borderless-capture",
"Title": "Deny borderless screen capture by Windows apps",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessGraphicsCaptureWithoutBorder",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessgraphicscapturewithoutborder",
"Impact": "Strict denies covered apps capture without the Windows capture border; recording workflows can change.",
"Exceptions": ["LetAppsAccessGraphicsCaptureWithoutBorder_ForceAllowTheseApps", "LetAppsAccessGraphicsCaptureWithoutBorder_UserInControlOfTheseApps"]
},
{
"Id": "app-human-presence",
"Title": "Deny Windows apps human-presence sensing",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessHumanPresence",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccesshumanpresence",
"Impact": "Strict denies covered apps presence-sensor access; presence-aware lock, wake and attention features can be affected.",
"Exceptions": ["LetAppsAccessHumanPresence_ForceAllowTheseApps", "LetAppsAccessHumanPresence_UserInControlOfTheseApps"]
},
{
"Id": "app-motion",
"Title": "Deny Windows apps motion data",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessMotion",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessmotion",
"Impact": "Strict denies covered apps accelerometer and motion data.",
"Exceptions": ["LetAppsAccessMotion_ForceAllowTheseApps", "LetAppsAccessMotion_UserInControlOfTheseApps"]
},
{
"Id": "app-notifications",
"Title": "Deny Windows apps notification access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessNotifications",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessnotifications",
"Impact": "Strict denies covered apps access to other apps' notifications; automation and notification aggregation can fail.",
"Exceptions": ["LetAppsAccessNotifications_ForceAllowTheseApps", "LetAppsAccessNotifications_UserInControlOfTheseApps"]
},
{
"Id": "app-phone",
"Title": "Deny Windows apps phone access",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessPhone",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessphone",
"Impact": "Strict denies covered apps access to telephony state and functions.",
"Exceptions": ["LetAppsAccessPhone_ForceAllowTheseApps", "LetAppsAccessPhone_UserInControlOfTheseApps"]
},
{
"Id": "app-radios",
"Title": "Deny Windows apps radio control",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsAccessRadios",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsaccessradios",
"Impact": "Strict denies covered apps control of radios such as Bluetooth; it does not power off radio hardware.",
"Exceptions": ["LetAppsAccessRadios_ForceAllowTheseApps", "LetAppsAccessRadios_UserInControlOfTheseApps"]
},
{
"Id": "app-background",
"Title": "Deny Windows apps background execution",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\AppPrivacy",
"Name": "LetAppsRunInBackground",
"Standard": 0,
"Strict": 2,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#letappsruninbackground",
"Impact": "Strict prevents covered Windows apps from running in the background; notifications and synchronization can be delayed or stop.",
"Exceptions": ["LetAppsRunInBackground_ForceAllowTheseApps", "LetAppsRunInBackground_UserInControlOfTheseApps"]
},
{
"Id": "consumer-experiences",
"Title": "Disable Microsoft consumer experiences where supported",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\CloudContent",
"Name": "DisableWindowsConsumerFeatures",
"Standard": null,
"Strict": null,
"EnterpriseValue": 1,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-experience#allowwindowsconsumerfeatures",
"Impact": "Enterprise and Education only: prevents Microsoft consumer experiences and suggested app promotion. Pro and Home are not claimed as supported.",
"Exceptions": []
},
{
"Id": "windows-spotlight",
"Title": "Disable Windows Spotlight where supported",
"Hive": "CurrentUser",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\CloudContent",
"Name": "DisableWindowsSpotlightFeatures",
"Standard": null,
"Strict": null,
"EnterpriseValue": 1,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-experience#allowwindowsspotlight",
"Impact": "Enterprise and Education only: disables Windows Spotlight for the current user, including rotating cloud content and suggestions.",
"Exceptions": []
},
{ {
"Id": "delivery-optimization", "Id": "delivery-optimization",
"Title": "Restrict Delivery Optimization", "Title": "Restrict Delivery Optimization",
@@ -134,6 +618,42 @@
"Impact": "Standard: HTTP without peering. Strict: HTTP without peering or DO cloud service. Neither blocks all update traffic or guarantees suppression of GDID reporting.", "Impact": "Standard: HTTP without peering. Strict: HTTP without peering or DO cloud service. Neither blocks all update traffic or guarantees suppression of GDID reporting.",
"Exceptions": [] "Exceptions": []
}, },
{
"Id": "block-consumer-microsoft-auth",
"Title": "Block consumer Microsoft-account authentication in apps and services",
"Path": "SOFTWARE\\Policies\\Microsoft\\MicrosoftAccount",
"Name": "DisableUserAuth",
"Standard": 0,
"Strict": 1,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-msapolicy#microsoftaccount_disableuserauth",
"Impact": "Strict prevents applications and services from authenticating with consumer Microsoft accounts after cached tokens expire. It does not change the Windows logon account or browser-based authentication.",
"Exceptions": []
},
{
"Id": "block-adding-microsoft-accounts",
"Title": "Block adding Microsoft accounts to Windows",
"Path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System",
"Name": "NoConnectedUser",
"Standard": 0,
"Strict": 1,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/accounts-block-microsoft-accounts",
"Impact": "Strict blocks adding new Microsoft accounts while preserving existing Windows logon accounts. Value 3 is deliberately not used because it can prevent an existing Microsoft-account user from signing in to Windows.",
"Exceptions": []
},
{
"Id": "block-workplace-registration",
"Title": "Block adding work or school accounts to the device",
"Path": "SOFTWARE\\Policies\\Microsoft\\Windows\\WorkplaceJoin",
"Name": "BlockAADWorkplaceJoin",
"Standard": 0,
"Strict": 1,
"EnterpriseValue": null,
"Source": "https://learn.microsoft.com/en-us/entra/identity/devices/howto-device-identity-virtual-desktop-infrastructure",
"Impact": "Strict blocks new Microsoft Entra workplace registration. Existing organizational enrollment is not removed; ms-private refuses mutation on domain-joined or MDM-enrolled devices.",
"Exceptions": []
},
{ {
"Id": "updates-auto", "Id": "updates-auto",
"Title": "Automatic Windows Update choice", "Title": "Automatic Windows Update choice",
+164
View File
@@ -0,0 +1,164 @@
{
"Schema": 2,
"FreshInstallMaxAgeDays": 7,
"DefaultFreshPreset": "FreshInstall",
"DefaultEstablishedPreset": "EstablishedSafe",
"Presets": [
{
"Id": "EstablishedSafe",
"Label": "Established PC - conservative",
"Description": "For an existing daily-use computer. Applies only the Standard policy profile and leaves applications, accounts, services, devices, Recall data, files and network behavior unchanged.",
"Profile": "Standard",
"Updates": "Keep",
"Services": false,
"Accounts": false,
"Browsers": [],
"RemoveBundled": false,
"RemovePersonal": false,
"RemoveStore": false,
"RemoveOneDrive": false,
"RemoveEdge": false,
"Recall": false,
"RemoveRecall": false,
"Network": "Observe",
"RollbackMinutes": 10,
"Cleanup": false,
"PrivacyCleanup": false,
"CleanupSchedule": false,
"Lock": false,
"InstallMullvad": false,
"InstallVeraCrypt": false,
"DisableSensors": false
},
{
"Id": "FreshInstall",
"Label": "Fresh install - complete privacy setup",
"Description": "For a new Windows installation. Applies Strict privacy, removes every unambiguously removable app including Store, Edge and OneDrive, purges Microsoft app credentials, removes Recall, disables reviewed services and detected sensors, blocks reviewed optional endpoints, cleans artifacts and installs Mullvad, VeraCrypt, Zen and Tor.",
"Profile": "Strict",
"Updates": "Automatic",
"Services": true,
"Accounts": true,
"Browsers": ["Zen", "Tor"],
"RemoveBundled": true,
"RemovePersonal": true,
"RemoveStore": true,
"RemoveOneDrive": true,
"RemoveEdge": true,
"Recall": true,
"RemoveRecall": true,
"Network": "BlockOptionalTelemetry",
"RollbackMinutes": 10,
"Cleanup": true,
"PrivacyCleanup": true,
"CleanupSchedule": true,
"Lock": true,
"InstallMullvad": true,
"InstallVeraCrypt": true,
"DisableSensors": true
},
{
"Id": "Gentle",
"Label": "Gentle",
"Description": "Core privacy policy and curated promotional-app removal. Keeps personal apps, Store, Edge, OneDrive, accounts, sensors and network behavior.",
"Profile": "Standard",
"Updates": "Keep",
"Services": false,
"Accounts": false,
"Browsers": ["Zen"],
"RemoveBundled": true,
"RemovePersonal": false,
"RemoveStore": false,
"RemoveOneDrive": false,
"RemoveEdge": false,
"Recall": false,
"RemoveRecall": false,
"Network": "Observe",
"RollbackMinutes": 10,
"Cleanup": false,
"PrivacyCleanup": false,
"CleanupSchedule": false,
"Lock": false,
"InstallMullvad": false,
"InstallVeraCrypt": false,
"DisableSensors": false
},
{
"Id": "Medium",
"Label": "Medium - automatic removal",
"Description": "Strict privacy, account purge, Recall removal, privacy services, cleanup and every unambiguously removable app including Microsoft Store, Edge, OneDrive and third-party apps. Security and runtime dependencies remain protected.",
"Profile": "Strict",
"Updates": "Automatic",
"Services": true,
"Accounts": true,
"Browsers": ["Zen"],
"RemoveBundled": true,
"RemovePersonal": true,
"RemoveStore": true,
"RemoveOneDrive": true,
"RemoveEdge": true,
"Recall": true,
"RemoveRecall": true,
"Network": "BlockOptionalTelemetry",
"RollbackMinutes": 10,
"Cleanup": true,
"PrivacyCleanup": true,
"CleanupSchedule": true,
"Lock": true,
"InstallMullvad": true,
"InstallVeraCrypt": true,
"DisableSensors": false
},
{
"Id": "Strong",
"Label": "Strong - automatic plus sensors",
"Description": "Medium plus Tor Browser and automatic selection of camera, microphone and sensor devices. Windows Hello or conferencing hardware can be disabled.",
"Profile": "Strict",
"Updates": "Automatic",
"Services": true,
"Accounts": true,
"Browsers": ["Zen", "Tor"],
"RemoveBundled": true,
"RemovePersonal": true,
"RemoveStore": true,
"RemoveOneDrive": true,
"RemoveEdge": true,
"Recall": true,
"RemoveRecall": true,
"Network": "BlockOptionalTelemetry",
"RollbackMinutes": 10,
"Cleanup": true,
"PrivacyCleanup": true,
"CleanupSchedule": true,
"Lock": true,
"InstallMullvad": true,
"InstallVeraCrypt": true,
"DisableSensors": true
},
{
"Id": "Maximum",
"Label": "Maximum - timed isolation",
"Description": "Strong plus all three browsers and default-outbound network isolation with a mandatory timed rollback. Retaining isolation prevents normal online updates until connectivity is restored or explicitly allowed.",
"Profile": "Strict",
"Updates": "Notify",
"Services": true,
"Accounts": true,
"Browsers": ["Zen", "Helium", "Tor"],
"RemoveBundled": true,
"RemovePersonal": true,
"RemoveStore": true,
"RemoveOneDrive": true,
"RemoveEdge": true,
"Recall": true,
"RemoveRecall": true,
"Network": "Isolate",
"RollbackMinutes": 10,
"Cleanup": true,
"PrivacyCleanup": true,
"CleanupSchedule": true,
"Lock": true,
"InstallMullvad": true,
"InstallVeraCrypt": true,
"DisableSensors": true
}
]
}
+89
View File
@@ -0,0 +1,89 @@
{
"Schema": 1,
"Services": [
{
"Id": "connected-telemetry",
"Name": "DiagTrack",
"Instances": false,
"SetUserServiceFlags": false,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Stops the Connected User Experiences and Telemetry service. Required service traffic and application telemetry remain separate.",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "wap-push-routing",
"Name": "dmwappushservice",
"Instances": false,
"SetUserServiceFlags": false,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Stops WAP push message routing used by device-management scenarios. Managed devices are rejected by ms-private before mutation.",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services"
},
{
"Id": "connected-devices-platform",
"Name": "CDPSvc",
"Instances": false,
"SetUserServiceFlags": false,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Breaks cross-device and connected-device experiences. This is evidence of a stopped local component, not proof that every GDID-like identifier is suppressed.",
"Source": "https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows"
},
{
"Id": "connected-devices-user",
"Name": "CDPUserSvc",
"Instances": true,
"SetUserServiceFlags": true,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Prevents future per-user CDP instances and stops current instances. Nearby Sharing, Phone Link, Miracast/DLNA/DIAL and related device experiences can stop working.",
"Source": "https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows"
},
{
"Id": "cloud-sync-user",
"Name": "OneSyncSvc",
"Instances": true,
"SetUserServiceFlags": true,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Disables the per-user host for mail, contacts, calendar and other synchronized user data.",
"Source": "https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows"
},
{
"Id": "cloud-backup-user",
"Name": "CloudBackupRestoreSvc",
"Instances": true,
"SetUserServiceFlags": true,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Disables Windows cloud backup and restore monitoring for user application and setting state.",
"Source": "https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows"
},
{
"Id": "location-service",
"Name": "lfsvc",
"Instances": false,
"SetUserServiceFlags": false,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Disables Windows geolocation service in addition to policy. IP-based location and application-specific collection remain possible.",
"Source": "https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints"
},
{
"Id": "windows-error-reporting",
"Name": "WerSvc",
"Instances": false,
"SetUserServiceFlags": false,
"Standard": "Keep",
"Strict": "Disabled",
"Impact": "Stops Windows Error Reporting service. Crash diagnosis and automatic solution lookup are reduced.",
"Source": "https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings"
}
],
"ProtectedServices": [
"wuauserv", "UsoSvc", "BITS", "CryptSvc", "WinDefend", "WdNisSvc", "SecurityHealthService",
"mpssvc", "BFE", "EventLog", "wlidsvc", "LicenseManager", "ClipSVC", "WpnService", "WpnUserService"
]
}
+34
View File
@@ -0,0 +1,34 @@
# Anonymity is an operating practice
ms-private reduces named Windows collection and local retention. It cannot make a Windows device anonymous by itself. An observer can correlate an IP address, an account, a browser fingerprint, a payment, a document, a writing pattern, a contact graph or behavior even when an operating-system identifier is absent.
## Choose the network model before the preset
For ordinary privacy, keep security updates enabled, use a reputable encrypted connection on untrusted networks, and inspect the traffic that remains. A VPN moves trust from the access network to the VPN provider; it does not remove account identity or browser identifiers.
For unlinkable web sessions, use an unmodified Tor Browser and keep those sessions separate from everyday accounts. Tor Browser only routes its own traffic. Signing into an identifying account, opening downloaded documents in networked external applications, installing extensions or changing fingerprint-sensitive settings can link the session. Follow the [Tor Project's safety guidance](https://support.torproject.org/tor-browser/security/using-tb-safely/).
For a Windows environment that must make no direct Internet connections, use ms-private's timed isolation only after preparing local recovery. Obtain signed updates on a separate maintained collector, verify them, and transfer them offline. This model trades cloud reputation, simple servicing and live certificate checks for containment. The collector still contacts publishers.
## Separate identities deliberately
- Use different OS users or virtual machines for identities that must not be linked. A different browser profile is a convenience boundary, not a strong isolation boundary.
- Do not sign into the same email, social, Microsoft, payment or recovery accounts across identities.
- Keep phone numbers, recovery addresses, usernames, avatars, writing patterns, contacts and posting schedules in the threat model.
- Remove document metadata and review visible content before publishing. Screenshots can reveal account names, notifications, time zone, language, monitor layout and other context.
- Use unique passwords and phishing-resistant MFA. CISA's Scattered Spider advisory shows that identity recovery and help-desk social engineering can defeat strong endpoint controls when human processes remain weak.
## Keep security evidence and maintenance
Do not delete Defender, firewall, authentication or Windows Update event channels as routine privacy cleanup. Those logs help detect account takeover and malware, and deleting them does not recall data already transmitted. Restrict access and retention according to the threat model instead.
Preserve Defender, the firewall, Secure Boot, TPM-backed protections, BitLocker or device encryption, browser updates and Windows security updates. Run `Security.ps1` after applying a preset and after every major Windows update. Use `Network.ps1 -Mode Validate` and a short packet capture to measure the result on the exact Windows build.
## Publish claims that the evidence supports
- **Selected**: the control is in a plan.
- **Applied**: Windows accepted the change and a local readback matched.
- **Observed**: a controlled capture or external test measured the expected behavior.
- **Not observed**: a test did not see a flow during its limited window; this is not proof that it can never occur.
Identifier rotation is not an anonymity control in ms-private. A server can restore an identifier, correlate a series of values, or use hardware, account and network signals. Randomly rewriting an undocumented value can also break identity, licensing or updates. The project disables documented channels, removes selected account state, narrows outbound connections, and asks for packet-capture evidence instead.
+102
View File
@@ -0,0 +1,102 @@
# Why I started taking Windows privacy more seriously
> “There is a sacred realm of privacy for every man and woman where he makes his choices and decisions—a realm of his own essential rights and liberties into which the law … must not intrude.”
Those are the words of Geoffrey Fisher, quoted by Patrick Devlin in *The Enforcement of Morals*, Chapter 7, “Mill on Liberty in Morals.”
Recently I began reading more material from [CISA](https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a), the [U.S. Department of Justice](https://www.justice.gov/usao-ndil/pr/alleged-member-criminal-cyber-hacking-group-scattered-spider-arrested-finland-and) and [MITRE ATT&CK](https://attack.mitre.org/versions/v17/groups/G1015/). What pushed me further was a vx-underground post about Peter Stokes, an alleged member of Scattered Spider, and the court papers behind it.
The case made me think less about the privacy switches Windows shows to users and more about the records that exist behind those switches.
The criminal complaint is an allegation, not evidence of guilt. Stokes is presumed innocent unless proven guilty. That legal distinction matters, and so does the technical distinction between what one investigation describes and what every Windows computer necessarily does.
## The identifier that caught my attention
The [federal complaint](https://www.justice.gov/usao-ndil/media/1450651/dl?inline=) describes Microsoft records that associated an installation identifier with URLs, IP addresses and timestamps. The identifier was referred to as GDID, or Global Device Identifier.
I had barely heard of GDID before this. It sounded like one more obscure value hidden somewhere inside Windows, but the important part was the association: an identifier becomes much more revealing when it is connected to network addresses, activity and time.
Microsoft does publicly document a `GlobalDeviceId` field in its Delivery Optimization reporting schema. The same table includes fields such as device name, ISP, approximate location derived from IP address and the last time the device sent data to Microsoft. Microsoft describes the field as an identifier used internally. [Microsoft UCDOStatus reference](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/ucdostatus).
That official documentation proves that a Microsoft global device identifier exists in at least that reporting context. It does not prove that the UCDOStatus table produced the web records described in the complaint, or that every Windows installation reports the same information in the same way.
While looking for more information, I found [gdid-reversal](https://github.com/SmtimesIWndr/gdid-reversal), its follow-up [We-running-GDID-back](https://github.com/SmtimesIWndr/We-running-GDID-back), and the [ZeroTrace Lab GDID article](https://www.zerotracelab.com/blog/gdid-windows-tracking). These projects discuss local LID values, PUID formatting, Microsoft identity services and possible hardware associations.
They are useful reverse-engineering leads, but they are not Microsoft specifications. Their results should be reproduced on controlled Windows builds before being turned into universal claims.
### My current GDID understanding
| Question | Current answer |
| --- | --- |
| Does Microsoft use something called GlobalDeviceId? | Yes. Microsoft documents the field in its reporting schema. |
| Is GDID just one registry value? | The independent research identifies a local LID representation, but the local value should not be treated as the complete server-side identity. |
| Can changing that value make a device anonymous? | There is no evidence that changing it alone removes Microsoft’s backend associations. A service may restore it or correlate the device through other signals. |
| Would generating a new value every minute help? | Only if every relevant service trusted the new value and discarded every other signal. That has not been demonstrated, and continuous mutation could damage authentication, licensing or updates. |
| Can a reinstall or virtual machine guarantee a fresh identity? | It can change local observations, but it does not prove that a remote provider cannot recognize the physical device, account or network. |
| Can previously collected records be deleted locally? | No. Local cleanup cannot recall records already transmitted to a provider. |
The most defensible conclusion is that GDID is one signal inside a larger identity and telemetry system. Treating it as a single registry switch would give me more confidence than evidence.
## GDID is only one part of the problem
Microsoft’s own optional-diagnostics documentation says covered data can include browser IDs, URLs and search terms, page titles, notification text, and processed typing, inking or speech context. The browsing examples are scoped to Microsoft browsers and the data is described as pseudonymized. Those qualifications matter, but so does the breadth of the documented categories. [Optional diagnostic data](https://learn.microsoft.com/en-us/windows/privacy/optional-diagnostic-data).
Windows also uses other installation, hardware, account, network and event-correlation fields. Connected experiences, cloud synchronization, application analytics and security services have different data paths. Turning off an advertising ID or one diagnostic setting cannot address all of them.
Some of the strongest identifiers are also the least mysterious. An email account, phone number, payment, recovery address, browser synchronization profile or repeated username can join activity more reliably than an obscure operating-system field.
## Privacy without abandoning security
I want to reduce unnecessary collection without turning the computer into an unmaintained target.
Windows Update, browser updates, antivirus, the firewall, disk encryption and supported boot protections still matter. A large blocklist can look impressive while silently breaking certificate maintenance, malware reputation, application updates or operating-system servicing.
There is no way to download an update directly from Microsoft while sending literally nothing to Microsoft. The connection itself exposes an address and timing, and the request contains information needed to deliver the correct package. A VPN changes who sees the source connection, but it does not erase identifying content carried inside a request.
A machine that must make no direct Internet connections needs a different operating model: another maintained computer downloads reviewed, signed packages and transfers them for offline installation. That model has costs. It needs a repeatable process for Windows, Defender, browsers, certificates, firmware and recovery components. It also gives up live reputation and cloud-analysis features.
Privacy controls should therefore state what they disable, what they preserve and what evidence would show that they worked.
## The person remains part of the system
The more I read about Scattered Spider, the more obvious it became that endpoint settings are only part of the threat model. CISA describes social engineering, SIM swapping, stolen personal information and help-desk manipulation as central parts of the group’s operations.
People reveal information through conversations, screenshots, documents, usernames, writing style, posting times, contacts and account-recovery choices. Reusing the same identities across supposedly separate environments reconnects them. Signing into a personal account through Tor identifies the session to that account even if the network path is hidden.
This does not make technical privacy controls pointless. It means my habits, account boundaries and published content must be reviewed alongside the operating system.
Before publishing a screenshot or document, I now want to ask:
- Does it contain account names, notifications, metadata or a unique directory path?
- Does the language, time zone or posting schedule connect it to another identity?
- Am I reusing an email address, phone number, recovery account, avatar or username?
- Will opening a downloaded file cause another application to connect outside the protected browser?
- Am I about to describe a privacy result that I configured but never measured?
## A small tool grew out of the research
I started building **ms-private** to turn this research into a reviewable Windows configuration instead of another opaque “disable everything” script. It has an automatic Fresh Install or conservative Established PC starting point plus four manual intensity presets, a graphical wizard, an arrow-key terminal interface, exact app and sensor selection, recovery journals, network capture, cleanup and security-preservation checks. The normal release is one elevated Windows executable with the reviewed runtime embedded and verified before launch.
The tool reduces documented collection, connected features, application identity state and local retention. It can remove selected applications, purge current-user Microsoft application sign-ins, disable reviewed privacy services, apply supported policies, restrict optional endpoints, disable exact sensors and use timed network isolation. It does not claim to delete Microsoft’s server-side GDID records or make Windows anonymous.
The detailed presets, commands, recovery behavior, GDID scope and validation status live in the separate [ms-private tool breakdown](../../docs/TOOL-BREAKDOWN.md). Keeping that material outside this post lets the article remain about why I started the project and what the evidence changed in my thinking.
## What I took from this
Privacy is not one button, one browser or one identifier. It is a maintained boundary between the information a device produces, the services it contacts, the accounts I use and the details I reveal myself.
My goal is not to promise invisibility. It is to remove collection I do not need, reduce local traces I can responsibly control, keep the machine secure, separate identities where that matters, and describe every result with enough precision that someone else can test it.
## Sources
- [U.S. v. Peter Stokes — superseding complaint](https://www.justice.gov/usao-ndil/media/1450651/dl?inline=)
- [DOJ announcement concerning Peter Stokes](https://www.justice.gov/usao-ndil/pr/alleged-member-criminal-cyber-hacking-group-scattered-spider-arrested-finland-and)
- [CISA/FBI Scattered Spider advisory AA23-320A](https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-320a)
- [MITRE ATT&CK Scattered Spider entry](https://attack.mitre.org/versions/v17/groups/G1015/)
- [Microsoft UCDOStatus schema](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/ucdostatus)
- [Microsoft optional diagnostic data documentation](https://learn.microsoft.com/en-us/windows/privacy/optional-diagnostic-data)
- [Microsoft Windows 11 connection endpoints](https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints)
- [gdid-reversal](https://github.com/SmtimesIWndr/gdid-reversal)
- [We-running-GDID-back](https://github.com/SmtimesIWndr/We-running-GDID-back)
- [ZeroTrace Lab: GDID — The Windows Global Device Identifier](https://www.zerotracelab.com/blog/gdid-windows-tracking)
- [vx-underground post that prompted the investigation](https://x.com/vxunderground/status/2073427396535963967)
+1 -1
View File
@@ -37,7 +37,7 @@ Microsoft's [per-user services documentation](https://learn.microsoft.com/en-us/
## Verification performed ## Verification performed
PowerShell parsed the attachment without executing it. Three isolated in-memory probes confirmed: the real marker fails the script's search pattern; an unmatched start marker drops trailing synthetic entries; ASCII conversion changes a synthetic Unicode comment. No service, policy, hosts file, cache or network setting was modified. Results are in `reports/upstream-review/pasted-script-static-probes.json`. PowerShell parsed the attachment without executing it. Three isolated in-memory probes confirmed: the real marker fails the script's search pattern; an unmatched start marker drops trailing synthetic entries; ASCII conversion changes a synthetic Unicode comment. No service, policy, hosts file, cache or network setting was modified. The findings are summarized here; transient local probe reports are intentionally excluded from the repository.
## Integration decision ## Integration decision
+6 -4
View File
@@ -1,6 +1,6 @@
# Windows Home and browser setup guide # Windows Home and browser setup guide
These are manual controls, not a claim that the prototype has applied them. Open Windows Settings and use the pages below. Labels vary by Windows build. Keep this guide open while configuring; the tool does not automatically open pages, install applications or alter this host. These are Settings-backed controls; the machine-policy engine does not pretend to enforce them on Home. Run `Home.ps1 -Page <name>` or use the graphical wizard's Home tab to open the supported Windows Settings page. Labels vary by Windows build, so verify the visible toggle and representative behavior. ms-private does not automate localized clicks or mark a setting complete merely because it opened the page.
| Page / URI | Recommended choice | Verify | | Page / URI | Recommended choice | Verify |
| --- | --- | --- | | --- | --- | --- |
@@ -19,7 +19,9 @@ These are manual controls, not a claim that the prototype has applied them. Open
Also review Windows backup/settings sync, Phone Link, Widgets, online account use, notifications on the lock screen, browser location permissions and installed OEM utilities. A local Windows account reduces account coupling but is not an anonymity guarantee. Do not disable encryption or antivirus for performance/privacy presets. Also review Windows backup/settings sync, Phone Link, Widgets, online account use, notifications on the lock screen, browser location permissions and installed OEM utilities. A local Windows account reduces account coupling but is not an anonymity guarantee. Do not disable encryption or antivirus for performance/privacy presets.
On eligible devices, Recall settings are under Privacy & security > Recall & snapshots. Review deletion effects before disabling by policy or removing the feature. Existing snapshot data is not backed up by Wind. Use `Accounts.ps1 -Mode Plan -Scope AllAppAccounts` to preview current-user Microsoft application sign-outs. Apply permanently resets the listed app packages and deletes matching stored application credentials; it does not convert or remove the Windows logon account. Existing work or school registration must be reviewed under **Settings > Accounts > Access work or school** because ms-private refuses changes on a managed device.
On eligible devices, Recall settings are under Privacy & security > Recall & snapshots. Review deletion effects before disabling by policy or removing the feature. Existing snapshot data is not backed up by ms-private.
## Browser choices ## Browser choices
@@ -34,7 +36,7 @@ Install only the selected browsers; none becomes the default automatically:
./Install-Apps.ps1 -App Tor,Zen,Helium ./Install-Apps.ps1 -App Tor,Zen,Helium
``` ```
WinGet contacts its community source and publisher download infrastructure and can produce local logs. WinGet and publisher installers have their own diagnostic/privacy settings; this installer wrapper does not override them. These installation connections are separate from Wind's offline policy engine. Verify displayed package identity/version/publisher before accepting installer prompts. WinGet manifests are community-maintained; their presence is not a security audit. WinGet contacts its community source and publisher download infrastructure and can produce local logs. WinGet and publisher installers have their own diagnostic/privacy settings; this installer wrapper does not override them. These installation connections are separate from ms-private's offline policy engine. Verify displayed package identity/version/publisher before accepting installer prompts. WinGet manifests are community-maintained; their presence is not a security audit.
## Mullvad and storage ## Mullvad and storage
@@ -42,4 +44,4 @@ Mullvad installation is optional: `./Install-Apps.ps1 -App Mullvad`. It needs an
VeraCrypt installation is optional: `./Install-Apps.ps1 -App VeraCrypt`. Configure containers and unmount behavior within VeraCrypt. Keep system encryption and recovery keys; do not decrypt the OS as a prerequisite. Win+L does not wipe all RAM or revoke every session token. VeraCrypt installation is optional: `./Install-Apps.ps1 -App VeraCrypt`. Configure containers and unmount behavior within VeraCrypt. Keep system encryption and recovery keys; do not decrypt the OS as a prerequisite. Win+L does not wipe all RAM or revoke every session token.
The source-backed rationale and limitations are in [RESEARCH.md](RESEARCH.md). All recommendations need practical verification on the actual Windows build and hardware. The source-backed rationale and limitations are in [PRIVACY.md](PRIVACY.md). Verify controls on the actual Windows build and hardware.
+19
View File
@@ -0,0 +1,19 @@
# Native API and security-tool interaction
ms-private does not directly import `kernel32.dll`. An earlier read-only executable-inspection test used `LoadLibraryEx` and related `kernel32` resource functions; Microsoft Defender blocked that dynamically compiled inspection code. The test now parses PE headers and resources with managed byte operations, so those imports were removed rather than allowlisted or hidden.
The .NET runtime, Windows PowerShell and Windows themselves ultimately call native operating-system libraries. Avoiding the word `kernel32` cannot guarantee that a process never reaches those libraries, and ordinary use of a Windows API is not by itself a malware signal. The relevant questions are which privileged behavior is requested, whether the target is exact, whether the user approved it, and whether recovery and verification exist.
## Direct native bridge retained
`src/MsPrivate.Accounts.psm1` uses the documented WinCred functions in `advapi32.dll` to enumerate Credential Manager metadata and delete only targets matching `config/accounts.json`. The bridge is initialized only when account planning or purge is requested. Reports replace matching target names with stable local action IDs and never write account names, target strings or credential secrets.
The bridge remains because Windows PowerShell has no built-in, locale-independent cmdlet for exact generic Credential Manager enumeration and deletion. Replacing it with parsed `cmdkey.exe` output would make targeting depend on localized text. It must receive clean-VM Defender acceptance testing before public release; third-party EDR compatibility is optional and ms-private will not create antivirus exclusions or weaken AMSI to make it run.
Clipboard clearing now uses the managed WPF `PresentationCore` API. No direct `user32.dll` bridge remains.
## Changes that security products can observe
Applying a Medium, Strong or Maximum preset performs behaviors that security software may monitor: service configuration, policy and registry writes, firewall-rule creation, Appx or desktop uninstallation, Credential Manager deletion, scheduled-task creation, device disablement, Recall feature changes and file cleanup. UAC is expected. Defender or another EDR can warn, block or quarantine an unsigned administrative tool even when its intent is legitimate.
The project does not suppress those products. Release acceptance therefore includes testing the final executable with Defender enabled, no exclusions, every mutation group applied separately, recovery exercised, and detections recorded. Code signing, timestamping and release provenance remain roadmap items; signing can improve identity and reputation but does not guarantee that security software will allow privileged behavior.
+14 -12
View File
@@ -4,14 +4,16 @@
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 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.
## Profiles ## Profiles
| Profile | Default behavior | Availability | | Profile | Default behavior | Availability |
| --- | --- | --- | | --- | --- | --- |
| Standard | Minimum supported diagnostic policy, no advertising ID, Windows location/camera restrictions, covered app microphone default deny, no cloud speech/clipboard sync/Find My Device/cloud search, DO without peering; updates unchanged | Prototype implements policy configuration on reviewed Pro/Enterprise/Education build families | | Standard | Minimum supported diagnostic policy, no advertising ID, Windows location/camera restrictions, covered app microphone default deny, Activity Feed/upload off, no cloud speech/clipboard sync/Find My Device/cloud search, WER off and DO without peering; updates unchanged | Implements policy configuration on reviewed Pro/Enterprise/Education build families |
| Strict | Standard plus no clipboard history and DO mode 99 | Prototype implements those added policies; advanced networking is not included yet | | Strict | Standard plus denied app access to account, calendar, contacts, call history, email, messaging, diagnostics, trusted/unpaired devices and voice activation; no clipboard history; DO mode 99; selected connected/cloud/diagnostic services disabled; future consumer Microsoft app authentication and workplace registration blocked; current Microsoft app sign-ins selected for purge | Default wizard profile. Policies and services are reversible; account-package reset and credential deletion are irreversible and require a separate acknowledgement. |
| Isolated session | Separate Tails environment; explicit persistence and identity separation | Guidance; not an automated OS installer | | Isolated session | Separate Tails environment; explicit persistence and identity separation | Guidance; not an automated OS installer |
Users can select individual supported IDs with `-Id`. The future UI needs a clear restore action for each restriction so camera/microphone use is possible when wanted. Software permission controls and hardware disconnection must have distinct labels. Users can select individual supported IDs with `-Id`. The future UI needs a clear restore action for each restriction so camera/microphone use is possible when wanted. Software permission controls and hardware disconnection must have distinct labels.
@@ -23,18 +25,18 @@ Users can select individual supported IDs with `-Id`. The future UI needs a clea
| Policy engine | Audit, Plan, Apply, Restore, edition/build gates, readback, recovery journal and transaction tests | VM verification on each supported build/edition; confirm policy consumption after reboot | | Policy engine | Audit, Plan, Apply, Restore, edition/build gates, readback, recovery journal and transaction tests | VM verification on each supported build/edition; confirm policy consumption after reboot |
| Browser/app selection | Exact package IDs, optional interactive installer, no default takeover, `-WhatIf` | Verify freshness/publisher manifests in release pipeline; UI choices; detect updates without importing personal profiles | | Browser/app selection | Exact package IDs, optional interactive installer, no default takeover, `-WhatIf` | Verify freshness/publisher manifests in release pipeline; UI choices; detect updates without importing personal profiles |
| Home edition | Read-only report and explicit manual guide | Supported Settings integration; verify actual toggles rather than pretending Pro policies work | | Home edition | Read-only report and explicit manual guide | Supported Settings integration; verify actual toggles rather than pretending Pro policies work |
| Diagnostics | Supported minimum Windows policy | Separate crash reporting, feedback and app-level controls with sources and side effects | | Diagnostics | Supported minimum policy, feedback prompts off, WER policy/service off, reviewed endpoint rules and DiagTrack shutdown | Validate effective traffic on each supported build and retain required security/update channels |
| Connected experiences | A small documented subset | Backup/account sync, Phone Link/CDP, Widgets, Start web search, consumer suggestions, application-specific policies; validate edition and deployment mechanism | | Connected experiences | Activity upload policies, app capability restrictions, CDPSvc/CDPUserSvc, OneSyncSvc and CloudBackupRestoreSvc reversible shutdown | Validate reboot persistence and effects on Phone Link, Nearby Sharing, Miracast, mail/calendar/contact sync and cloud backup |
| Network controls | Research and validation specification only | Lab capture, destination attribution, narrow rules, timed recovery and VPN coexistence tests | | Network controls | Observe, reviewed optional-FQDN rules, timed default-outbound isolation, explicit confirmation, restore and Pktmon capture | Complete IPv4/IPv6/DNS/DoH/QUIC/VPN and external-gateway lab validation; expand endpoints only with provenance |
| AI removal | Research and deletion warning | Inventory Recall eligibility and installed Copilot apps; explicit data-deletion preview; dependency-aware uninstall | | AI removal | Recall inventory, deletion acknowledgement, supported policy and optional-feature disable/restore | Eligible-device deletion/reboot validation; installed Copilot inventory remains catalog-driven app selection |
| Debloat | Design only | Exact allowlist of removable apps; distinguish user/provisioned scope; OneDrive data-preservation preflight; Edge/WebView2 separation | | Debloat | Exact current-user inventory, curated bundled default, personal opt-in, browser-before-removal, OneDrive/Edge handling and protected dependencies | Live package/data/reinstall/new-user validation on every supported build |
| Updates | Keep, Automatic, Notify, Manual registry choices on eligible editions | Reboot and actual update workflow tests; UI temporary pause uses supported Windows mechanisms; browser/AV updates independent | | Updates | Keep, Automatic, Notify, Manual registry choices on eligible editions | Reboot and actual update workflow tests; UI temporary pause uses supported Windows mechanisms; browser/AV updates independent |
| Sensors | Location/camera policy and limited app microphone policy | Optional hardware-device disablement with reliable recovery and device selection; never disable audio input blindly | | Sensors | Location/camera policy, limited app microphone policy and exact selected PnP device disable/restore | Physical camera/microphone/Hello and restoration validation; nothing selected by default |
| Cleanup | Home guide | Storage Sense integration with explicit retention and exclusions; skip busy files; never clean the registry or clear security logs | | Cleanup | Preview-first allowlisted Temp, WER/crash, Recent/Jump List, thumbnail, PowerShell-history and expired-report cleanup; age threshold; reparse/change/lock checks; optional safe schedule | Live active-installer, locked-cache and long-running schedule validation; browser-owned retention remains separate |
| Lock behavior | Design only | Session-lock trigger, vault integrations, clipboard handling, selected VeraCrypt volumes, busy-volume/error reporting; no generic process kill | | Lock behavior | SessionLock scheduled task, current clipboard clear, exact selected VeraCrypt graceful unmount and local result report | Win+L/auto-lock/user-switch/RDP and busy-volume validation; password-manager adapters remain product-specific |
| Encryption | Guidance only | Read-only encryption status without recovery-key capture; key-custody guide; no automatic encryption/decryption migration | | Encryption | Guidance only | Read-only encryption status without recovery-key capture; key-custody guide; no automatic encryption/decryption migration |
| Security software | Preserve existing protections | Optional Bitdefender selection after conflict checks; no automatic Zillya recommendation pending sufficient evidence | | Security software | Preserve existing protections | Optional Bitdefender selection after conflict checks; no automatic Zillya recommendation pending sufficient evidence |
| Packaging | Local CLI scripts | Signed release, reproducible manifest, checksums, native GUI; optional AME wrapper after VM validation | | Packaging | Self-contained x64 WinExe with UAC manifest, hash-manifested embedded runtime and Program Files installation; local CLI, native TUI, tabbed Windows Forms wizard, source ZIP builder and AME wrapper/builder | VM-run executable and AME validation, code signing, reproducible CI artifacts and release provenance |
## Network design requirements ## Network design requirements
@@ -53,7 +55,7 @@ The requested no-direct-Microsoft-contact option now has a [separate collector a
2. Detect OS edition/build and managed-device constraints. Show unavailable controls as unavailable. 2. Detect OS edition/build and managed-device constraints. Show unavailable controls as unavailable.
3. Choose browsers and optional Mullvad/VeraCrypt; show installation separately from configuration. 3. Choose browsers and optional Mullvad/VeraCrypt; show installation separately from configuration.
4. Review each setting, its source, expected result, compatibility impact and reversibility. Separate data-deleting actions. 4. Review each setting, its source, expected result, compatibility impact and reversibility. Separate data-deleting actions.
5. Prepare a local recovery journal, then apply selected actions in small independent groups. 5. Prepare local recovery journals, then apply selected actions in recorded groups. Continue after an unrelated group fails; skip only a group whose declared prerequisite failed.
6. Report configured, pending reboot, behavior verified, unsupported, failed and conflict states separately. 6. Report configured, pending reboot, behavior verified, unsupported, failed and conflict states separately.
7. Offer rollback by group and show remaining manual checks. A successful registry write never earns a "verified anonymous" badge. 7. Offer rollback by group and show remaining manual checks. A successful registry write never earns a "verified anonymous" badge.
+23
View File
@@ -0,0 +1,23 @@
# Privacy scope and sources
Reviewed 2026-09-20. ms-private reduces selected collection and connected features. It cannot establish device anonymity. Optional diagnostics, required service data, signed-in accounts and third-party apps are separate channels. [Microsoft essential services](https://learn.microsoft.com/en-us/windows/privacy/essential-services-and-connected-experiences).
GDID appears in the Stokes complaint alongside URL records and corroborating account/IP evidence. The complaint does not identify the exact collection mechanism or prove every browser is monitored. Local identifier replacement is not evidence of unlinkability. ms-private disables CDPSvc, the CDPUserSvc template and current instances, Connected User Experiences and Telemetry, cloud sync/backup, location and WER in Strict mode; it also blocks the reviewed diagnostic endpoint catalog. This reduces plausible channels but is labelled “GDID mitigated, verification pending” until controlled packet captures demonstrate the effect. ms-private does not rotate an undocumented identifier. [Complaint, paragraphs 25-30](https://www.justice.gov/usao-ndil/media/1450651/dl?inline=), [Microsoft per-user services](https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows), [Windows endpoints](https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints).
The Windows advertising ID is not interchangeable with GDID, a Microsoft account ID, hardware identifiers or a browser fingerprint. Individual policy sources and impacts live in [policies.json](../config/policies.json). On Pro the diagnostic minimum used here retains required diagnostics; policy configuration is subject to edition and precedence. [Windows diagnostics](https://learn.microsoft.com/en-us/windows/privacy/configure-windows-diagnostic-data-in-your-organization).
Strict mode blocks new consumer Microsoft authentication by applications and new workplace registration. Its separate account purge resets exact current-user Store, Xbox, packaged Microsoft 365/Teams and shared account-broker packages, then deletes only matching stored Microsoft application credentials. Resetting a package permanently removes its current-user data and sign-in details. Reports omit identity names, emails, credential targets and secrets. The Windows logon identity and existing organizational registration are not removed. [Microsoft account authentication policy](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-msapolicy), [MSIX reset behavior](https://learn.microsoft.com/en-us/windows/msix/desktop/managing-your-msix-reset-and-repair).
Uninstalling OneDrive does not download cloud-only files. Make needed files available locally and back them up first. ms-private does not erase cloud records or manually remove synchronized folders. [OneDrive removal](https://support.microsoft.com/en-us/onedrive/turn-off-disable-or-uninstall-onedrive).
Edge removal is limited by Windows and regional support. Its enterprise uninstall policy is not a universal consumer-PC bypass. WebView2 remains because applications use it as a separate runtime. [Edge update policy](https://learn.microsoft.com/en-us/deployedge/microsoft-edge-update-policies#uninstall), [WebView2 distribution](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution).
Appx package removal is scoped to the current user; the general data-preservation switch is not available for normal installed Store packages. Desktop removal is delegated to the registered product through WinGet with product-code/scope filtering. [Remove-AppxPackage](https://learn.microsoft.com/en-us/powershell/module/appx/remove-appxpackage), [WinGet uninstall](https://learn.microsoft.com/en-us/windows/package-manager/winget/uninstall).
Online updates necessarily send requests. An isolated PC can receive separately collected offline packages, but loses live cloud services. The offline bundle verifier proves only agreement with supplied hashes and metadata, not publisher authenticity or update completeness. See [offline updates](OFFLINE-UPDATES.md).
Local deletion and network anonymity are separate. ms-private can preview and remove selected WER/crash data, Recent Items, Jump Lists, thumbnail caches, known Zen/Tor/Helium cache subdirectories, PowerShell history, temporary files and expired toolkit reports. Browser cleanup retains cookies, passwords, bookmarks, sessions and settings. It does not clear security, Defender, update, firewall or authentication event logs. Those records help detect compromise and do not prevent an identifier or account token from being sent before deletion. File deletion is not a reliable physical-erasure guarantee on SSDs, backups or synchronized storage.
`Security.ps1` reports BitLocker/device-encryption protection, Defender and cloud-protection posture, firewall, Secure Boot, TPM, VBS/Credential Guard, LSA, UAC, SMB/SMB1, remote access and protected-service state. It deliberately omits recovery keys, account names, Defender exclusions and secrets. Encryption protects data at rest; it does not hide network identifiers while the machine is unlocked and online.
Tor Browser, Zen and Helium serve different browsing needs. Installing a browser or VPN does not separate personal accounts, clear provider records or hide everything an endpoint sends. Avoid mixing identities and retain browser/security updates.
+15 -1
View File
@@ -110,6 +110,20 @@ Exclude registry cleaners, blanket log clearing, SSD free-space wiping and RAM t
WinUtil provides useful UI, app installation and reversible settings, but every preset needs review and version pinning. Do not chain multiple debloat tools on a machine. This prototype uses its own small, documented policy catalog, not an unreviewed execution of WinUtil. [WinUtil tweak guidance](https://github.com/ChrisTitusTech/winutil/blob/main/docs/src/content/docs/guides/tweaks.mdx). WinUtil provides useful UI, app installation and reversible settings, but every preset needs review and version pinning. Do not chain multiple debloat tools on a machine. This prototype uses its own small, documented policy catalog, not an unreviewed execution of WinUtil. [WinUtil tweak guidance](https://github.com/ChrisTitusTech/winutil/blob/main/docs/src/content/docs/guides/tweaks.mdx).
AME is a delivery mechanism. Its Privacy+ methodology describes removal of Windows Update, conflicting with the requirement for reversible update choices. A future Wind AME wrapper should invoke the same tested engine rather than duplicate settings or remove security services. [Privacy+ methodology](https://download.amelabs.net/Privacy%2B%20Testing%20Methodology.pdf). AME is a delivery mechanism. Its Privacy+ methodology describes removal of Windows Update, conflicting with the requirement for reversible update choices. The ms-private AME wrapper invokes the same tested engine rather than duplicating settings or removing security services. [Privacy+ methodology](https://download.amelabs.net/Privacy%2B%20Testing%20Methodology.pdf).
For sessions requiring an amnesic environment, prefer a separate Tails USB over promises about making ordinary Windows forget everything. Persistence must remain an explicit choice. Tails still cannot guarantee safety against malicious hardware or identifying user behavior. [How Tails works](https://tails.net/about/index.en.html). For sessions requiring an amnesic environment, prefer a separate Tails USB over promises about making ordinary Windows forget everything. Persistence must remain an explicit choice. Tails still cannot guarantee safety against malicious hardware or identifying user behavior. [How Tails works](https://tails.net/about/index.en.html).
## 9. Documented but easy-to-miss channels and security checks
The September 2026 review found more documented surfaces that belong in a privacy model. Microsoft lists separate connections for device metadata, downloadable fonts, Windows location, Widgets and Spotlight. The optional-diagnostics schema says optional data can include browser IDs, URLs and search terms, page titles, notification text, processed typing context and recognized ink. These are not evidence of a secret backdoor; they show why one telemetry switch or one identifier cannot describe the complete data surface. [Windows 11 endpoints](https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints), [optional diagnostic data](https://learn.microsoft.com/en-us/windows/privacy/optional-diagnostic-data).
The policy catalog now covers tailored experiences and implicit ink/typing collection for the current user; search location and dynamic highlights; Widgets; device metadata and font streaming; MSRT infection reports; and packaged-app access to location, foreground text, gaze, screen capture, human-presence sensing, motion, notifications, phone state, radios and background execution. Per-app allow overrides are treated as conflicts because a global-default label would otherwise overstate coverage. Spotlight and consumer-experience controls are selected only on Enterprise/Education, matching Microsoft's current edition support. [Privacy Policy CSP](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy), [Search Policy CSP](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search), [Experience Policy CSP](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-experience), [Widgets policy](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-newsandinterests).
Strict mode disables LLMNR through the documented DNS Client policy. This reduces a local name-resolution spoofing and credential-relay surface, but it can break legacy discovery and name resolution for printers or peer devices. mDNS, NetBIOS, WPAD, file sharing and discovery remain separate research and compatibility decisions; they are not silently disabled under the Microsoft-telemetry label. [DNS Client policy](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-dnsclient#pol-llmnr).
The security audit now observes Defender real-time, behavior, IOAV, network-inspection and cloud settings; TPM and Secure Boot; VBS/Credential Guard; LSA protection; UAC; SMB1, signing and guest-related configuration; remote access; and the update, certificate, Defender, firewall and event-log services. It excludes Defender exclusions, account names, keys and secrets. LSA protection and Credential Guard defend credentials, while current Windows 11 releases also strengthen SMB signing. These controls need compatibility testing before enforcement, so the release audits them instead of forcing settings that might cut off NAS, domain or accessibility workflows. [Advanced credential protection](https://learn.microsoft.com/en-us/windows/security/book/identity-protection-advanced-credential-protection), [Credential Guard](https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/how-it-works/), [SMB hardening](https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-security-hardening).
The network catalog adds exact feature endpoints for location inference, metadata, font streaming, Widgets and Spotlight. It rejects target names under Windows Update, update delivery, Defender, SmartScreen, Store revocation and WNS families. NCSI, certificate revocation, root certificate maintenance, Windows Update, Defender and notification infrastructure remain available because blocking them can create false offline state or weaken security. A reset or feature update can also communicate before local policy is reapplied, so a high-assurance no-direct-contact design needs offline policy application or a physically/network-isolated Windows system plus a separate update collector. [Manage Windows connections](https://learn.microsoft.com/en-gb/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services).
Historical forensic research found hardware serials, external-storage traces and process-execution traces in older Windows telemetry resource files. That is a useful lab lead, not proof that the same artifacts or transmissions exist on every current build. Current claims require a named Windows build, a repeatable workflow, local and external capture, and redacted evidence. ETW patching, TLS interception and deletion of security logs are excluded because they impair defenses or create a new interception risk rather than proving privacy. [Forensic analysis of Windows telemetry](https://arxiv.org/abs/2002.12506).
+25
View File
@@ -0,0 +1,25 @@
# ms-private 0.0.1 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.
## Open work
| ID | Priority | Work still required |
| --- | --- | --- |
| R01 | P0 | Run the complete Windows Home, Pro, Enterprise and Education VM matrix, including supported Windows 10 servicing and reviewed Windows 11 builds. |
| R03 | P0 | Record controlled baseline and configured traffic at the host and an external gateway across boot, idle, browsing, updates, VPN and restore. |
| R13 | P1 | Build a versioned third-party application privacy register with publisher sources and separate update/security decisions. |
| R18 | P2 | Repeat validation after Windows feature/security updates and browser, VPN or installer changes. |
| R20 | P1 | Reproduce context-specific GDID/PUID, Widgets and Store observations with disposable identities and redacted evidence. |
| R21 | P0 | Validate the offline update design end to end on an isolated target. |
| R22 | P0 | Implement and validate the signed offline Windows Update Agent catalog scanner. |
| R23 | P0 | Cover Defender engine/platform/intelligence, certificate trust, browsers, applications, drivers and firmware in offline maintenance. |
| R24 | P1 | Authenticate collector downloads and transferred packages with publisher signatures, current catalogs and protected manifests. |
| R25 | P0 | Add Authenticode signing, trusted timestamping, SBOM/provenance output and a controlled release pipeline. |
| R26 | P0 | Exercise every privileged action with the built-in Defender protections enabled and no exclusions; optionally record third-party EDR compatibility without adding bypasses. |
## Implementation complete; live acceptance remains
R02, R04–R12 and R14–R17 have working code or prepared material, but their acceptance evidence is incomplete. This includes security-preservation checks, endpoint provenance, timed firewall recovery, real registry/service restoration, Home Settings verification, physical sensor testing, Recall and app-removal recovery, cleanup and lock-event testing, GDID research reproduction, pinned upstream artifacts, browser/VPN acceptance, GUI accessibility, AME execution and the final editorial check.
R19 is complete: the unsafe supplied-script patterns were reviewed and were not integrated. ms-private uses exact targets, write-ahead recovery records, protected components, readback and explicit destructive acknowledgements instead.
+20 -8
View File
@@ -1,6 +1,6 @@
# Delivery status — 2026-09-18 # Delivery status — 2026-09-19
Update architecture supplement completed 2026-09-19: local OS inventory and offline bundle verification are implemented in `Offline-Updates.ps1` and `src/Wind.OfflineUpdates.psm1`. Nineteen new tests passed in PowerShell 7 and Windows PowerShell 5.1. Real host inventory returned only OS version/edition/architecture. No real package download, authenticity validation, applicability scan, installation or network isolation was performed. The register now contains 85 controls/decisions, 21 identifier/data-channel entries, 19 tool/research entries, 20 claims, 24 remaining tasks, 12 supplied-script findings and a separate update-maintenance sheet. See `docs/OFFLINE-UPDATES.md` and `docs/ADDITIONAL-CHANNELS.md`. 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.
A sourced blog draft and pinned-revision GDID research review are included. Workbook formulas were recalculated and tested with temporary status edits, then restored; rendered sheets and exported structure were inspected. No Excel desktop execution was performed. A sourced blog draft and pinned-revision GDID research review are included. Workbook formulas were recalculated and tested with temporary status edits, then restored; rendered sheets and exported structure were inspected. No Excel desktop execution was performed.
@@ -11,15 +11,27 @@ The supplied script was parsed without execution. Three isolated in-memory probe
- Research and threat model with primary sources, implementation plan and release validation protocol. - Research and threat model with primary sources, implementation plan and release validation protocol.
- Three browser choices: Tor Browser, Zen and Helium; optional Mullvad and VeraCrypt installers. - Three browser choices: Tor Browser, Zen and Helium; optional Mullvad and VeraCrypt installers.
- Local PowerShell policy engine: read-only audit, plan, explicit apply, recovery journal and conflict-aware restore. - Local PowerShell policy engine: read-only audit, plan, explicit apply, recovery journal and conflict-aware restore.
- Home edition manual guide; no unsupported policy enforcement claims. - Home edition Settings assistant and manual guide; no unsupported policy enforcement claims.
- Tabbed Windows Forms wizard that reuses the PowerShell scripts, plus the arrow/Space/Enter terminal interface.
- Six-preset catalog with age-aware recommendation. Windows and profile ages of seven days or less select Fresh Install and its complete removal workflow; older/unknown installations select Established PC with no destructive groups. Gentle, Medium, Strong and Maximum remain manually selectable. Exact review and one final approval precede mutation.
- Complete-preset operation runner that records every selected group, continues with independent groups after a failure, skips only failed prerequisites and writes a local run summary without secrets.
- Exact current-user app inventory, protected dependency rules, default curated debloat, OneDrive/Edge ordering and Tor/Zen/Helium browser prerequisites.
- Optional network FQDN enforcement across 19 reviewed feature/telemetry endpoints, protected update/security endpoint rejection, explicit timed isolation, journal validation, post-check confirmation, Pktmon capture conversion and point-in-time socket inventory.
- Strict service engine for DiagTrack, dmwappushservice, CDPSvc/CDPUserSvc, OneSyncSvc, CloudBackupRestoreSvc, lfsvc and WerSvc, with original-state journals, instance shutdown and conflict-aware restore. Update, Defender, firewall, certificate, licensing, identity and Store security services are protected by catalog validation.
- Irreversible current-user Microsoft app-account purge: exact Store/Xbox/Microsoft 365/Teams/account-broker package resets plus allowlisted Credential Manager deletion, with account names and credential targets omitted from reports. Strict policy blocks future consumer app authentication and new workplace registration without disabling the Windows logon identity.
- Recall eligibility/policy/feature workflow with an irreversible-deletion acknowledgement.
- Exact selected PnP sensor disable/restore; allowlisted Temp, crash-report, Recent/Jump List, thumbnail, PowerShell-history and expired-report cleanup; scheduling; and SessionLock clipboard/VeraCrypt handling.
- Read-only BitLocker/device-encryption, Defender/cloud posture, firewall, Secure Boot, TPM, VBS/Credential Guard, LSA, UAC, SMB/SMB1, remote-access and protected-service status without recovery keys, account names, Defender exclusions or secrets.
- AME wrapper source and builder that launch the same graphical wizard without disabling Defender or Windows Update.
- Self-contained x64 `ms-private-0.0.1.exe` launcher with a `requireAdministrator` manifest, embedded hash-manifested runtime, exact-file verification, tamper repair and protected Program Files installation.
## Verification performed ## Verification performed
- 23 transaction/recovery tests passed in PowerShell 7.6.5. - 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.
- The same 23 tests passed in Windows PowerShell 5.1. - 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.
- Real journal create/atomic-replace/parse test passed using 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 audit on this Windows Home (`Core`) build 26200 reported all 10 Standard profile controls as unsupported for automatic policy enforcement. - Real read-only service planning completed in both PowerShell engines. Policy enforcement remains audit-only on this Windows Home (`Core`) build 26200.
- 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.
@@ -27,4 +39,4 @@ Compatibility testing found and corrected PowerShell 5.1 array enumeration diffe
## Remaining release work ## Remaining release work
The full wizard is not complete. VM policy behavior tests, physical sensor checks, measured network enforcement, GUI/AME packaging, data-preserving debloating, cleanup automation and lock-event integration are still pending as detailed in [PLAN.md](PLAN.md). No broad deployment or anonymity certification is implied by the prototype. The requested modules are implemented or packaged as explicit prepared controls. Production validation remains open: VM policy behavior, live app uninstall/reinstall, physical sensors and Windows Hello, network enforcement across IPv4/IPv6/DNS/DoH/QUIC/VPN, Pktmon/gateway comparison, Recall deletion/reboot, lock trigger paths, busy VeraCrypt volumes, cleanup with active installers, GUI accessibility/DPI, and AME execution. No broad deployment or anonymity certification is implied by this release candidate.
+133
View File
@@ -0,0 +1,133 @@
# ms-private tool breakdown
ms-private is a local Windows privacy and debloat toolkit. It builds an exact plan from the current machine, shows the plan before mutation, applies supported actions, and records local recovery or outcome data. It does not claim that Windows becomes anonymous or that Microsoft deletes previously collected records.
## Interfaces
| 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. |
| Graphical wizard | `ms-private-GUI.ps1` | Tabbed Windows Forms interface with one exact-plan review and final approval. |
| Terminal wizard | `ms-private.ps1` | Arrow, Space, Enter and Esc navigation with searchable app selection. |
| Complete preset CLI | `Preset.ps1` | Auto-detect or plan/apply EstablishedSafe, FreshInstall, Gentle, Medium, Strong or Maximum without navigating the UI. |
| Component CLIs | `Services.ps1`, `Accounts.ps1`, `Manage-Apps.ps1`, `Network.ps1`, `Hardware.ps1`, `Recall.ps1`, `Cleanup.ps1`, `Lock.ps1`, `Security.ps1` | Audit, plan, apply or restore one area. |
## Automatic presets
| Preset | Automatic selection |
| --- | --- |
| Established PC | Selected when Windows or the current profile is older than seven days, or age evidence is unavailable. Applies Standard policy only and keeps apps, accounts, services, devices, Recall, files, network and updates unchanged. |
| Fresh Install | Selected only when Windows and the current profile are both no more than seven days old. Runs the complete Strict removal and privacy workflow with Zen/Tor, Mullvad, VeraCrypt and detected sensors. |
| Gentle | Standard privacy policies and curated promotional-app removal. Keeps personal apps, Store, Edge, OneDrive, Microsoft app accounts, sensors and networking. |
| Medium | Strict policies and eight privacy-service templates, Microsoft app-account purge, Recall deletion/removal where supported, 19 reviewed optional endpoint rules, privacy cleanup, scheduled Temp cleanup, lock handling, Mullvad, VeraCrypt, and every unambiguously removable Microsoft or third-party app including Store, Edge and OneDrive. |
| Strong | Medium plus Tor Browser and exact selection of detected cameras, microphones and sensor devices. |
| Maximum | Strong plus Zen and Helium and timed default-outbound firewall isolation. The isolation rollback remains scheduled until the user confirms or restores the journal. |
Unsupported settings are shown as skipped. Conflicting registry types, stale app identities, forged plans and unavailable recovery mechanisms stop the affected workflow.
## Privacy policy engine
The policy catalog contains 56 reviewed controls. Standard and Strict cover diagnostic level, advertising ID, tailored experiences, feedback, Windows Error Reporting, activity history, online speech, typing and inking collection, cloud clipboard, cloud search, Microsoft app authentication, workplace registration, Widgets, search highlights, device metadata, downloadable fonts, Delivery Optimization, location and packaged-app access to camera, microphone, foreground text, screen capture, presence, gaze, motion, notifications, phones, radios and background execution.
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.
## Services and GDID-related reduction
Strict mode disables and stops these reviewed service templates:
- `DiagTrack`
- `dmwappushservice`
- `CDPSvc`
- `CDPUserSvc`
- `OneSyncSvc`
- `CloudBackupRestoreSvc`
- `lfsvc`
- `WerSvc`
Windows Update, Update Orchestrator, BITS, Cryptographic Services, Defender, firewall, Event Log, identity, licensing and security services are rejected if they enter a service-disable plan.
For GDID, ms-private reduces known or suspected transmission paths by disabling CDP and diagnostic services, removing connected applications, purging selected Microsoft app identity state, restricting optional endpoints and supporting packet capture. It does not rewrite the local `IdentityCRL\ExtendedProperties\LID`, rotate identifiers or claim server-side deletion. The accurate project status is **GDID transmission paths reduced; complete suppression unverified**.
## Applications, accounts and browsers
Gentle uses the curated package names in `config/debloat.json`. Fresh Install, Medium and stronger enumerate the installed inventory and select every unambiguously removable current-user Appx or registered desktop application. Established PC selects no application action.
The removal engine:
- Installs and verifies every selected replacement browser before the first removal.
- Uses exact Appx package identities or exact desktop product registrations.
- Rechecks name, version, publisher, scope and target immediately before removal.
- Removes ordinary apps before Store and Edge.
- Skips duplicate desktop name/scope registrations because they cannot be delegated safely to WinGet.
- Protects WebView2, drivers, firmware, runtimes, servicing, security products, authentication components, browsers, Mullvad, VeraCrypt and recovery infrastructure.
Appx removal applies to the current user. It does not silently deprovision packages for every future user. Desktop uninstallers follow their registered user or machine scope.
Account purge resets exact current-user Store, Xbox, packaged Microsoft 365/Teams and shared account-broker packages, then deletes only Credential Manager targets matching the reviewed Microsoft allowlist. Reports store action IDs and results without account names, email addresses, credential targets or secrets. The Windows logon identity and existing work/school enrollment remain separate.
Available browsers are Tor Browser, Zen and Helium. Tor Browser should remain unmodified and isolated from identifying accounts when unlinkability matters. Zen and Helium are everyday browser choices and do not create a separate identity merely because they are installed.
## Network controls
`Observe` changes nothing. `BlockOptionalTelemetry` creates dynamic-FQDN outbound rules for 19 reviewed Windows optional-feature or telemetry endpoints. Catalog validation rejects Windows Update, Defender, SmartScreen, certificate/revocation and WNS families that the project preserves.
`Isolate` schedules a local SYSTEM rollback first, then sets the default outbound action to Block for Domain, Private and Public firewall profiles. Existing explicit allow rules can still permit traffic. Confirming isolation removes the timer; restoring the journal returns the saved profile defaults.
Pktmon capture is explicit and local. Stop converts the trace to PCAPNG. Captures can expose private addresses and DNS names. Encrypted payloads remain encrypted, and one quiet capture cannot prove that an identifier is never sent.
## Recall and sensors
Recall planning checks edition, build, policy type and optional-feature state. Applying the supported disable policy deletes existing snapshots, so the workflow requires the deletion acknowledgement. Feature removal may require a reboot. Unsupported Recall actions are reported and skipped by complete presets; conflicting values remain a hard stop.
Hardware control operates on exact PnP instance IDs. Strong and Maximum automatically select detected cameras, microphones and sensors that report an enabled state. Disabling these devices can affect conferencing, Windows Hello and accessibility features. A journal records original device states for restoration.
## Cleanup and lock handling
Cleanup uses named allowlisted locations and rejects arbitrary roots. Categories cover aged Temp content, Windows Error Reporting queues and crash dumps, Recent Items and Jump Lists, unlocked thumbnail caches, PowerShell interactive history, known Zen/Tor/Helium cache subdirectories and expired non-journal ms-private reports. It checks reparse points, file age, changes and locks before deletion.
Cookies, passwords, bookmarks, sessions, Documents, Downloads, credential stores, recovery journals and Defender, firewall, authentication, update and Windows event logs remain protected. Ordinary deletion cannot guarantee physical erasure from an SSD, backup or synchronized provider.
The lock task clears the current clipboard and can request graceful unmount of selected non-system VeraCrypt drive letters. It does not terminate arbitrary applications or claim to wipe all secrets from RAM.
## Security and update preservation
`Security.ps1` audits BitLocker or device-encryption state, Defender and cloud protection, firewall profiles, Secure Boot, TPM, VBS/Credential Guard, LSA protection, UAC, SMB/SMB1, remote access and protected services. It does not read recovery keys, account names, Defender exclusions or secrets.
Online mode keeps Windows security updates available. A direct update necessarily sends connection and request metadata to Microsoft. The isolated update design uses a separate maintained collector, publisher-signed packages, an offline manifest and local integrity verification. The included verifier does not itself download, authenticate or install updates.
## Reports and recovery
Journals and reports are stored below `%LOCALAPPDATA%\MsPrivate`. Policy, service, network and device changes have restoration records. App removal, account-package reset, Recall snapshot deletion, credential deletion and cache deletion are not undone by those journals.
A complete preset also writes a run summary below `%LOCALAPPDATA%\MsPrivate\runs`. It records each group as succeeded, failed or skipped because a prerequisite failed. A failure in one group does not suppress unrelated groups; the summary never converts a partial run into a success.
Reports are local and have no automatic upload. Installed-app inventories and network captures can still be sensitive and should not be published without review.
## 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.
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.
For source development from the extracted ZIP:
```powershell
Get-ChildItem -LiteralPath . -Recurse -File | Where-Object { $_.Extension -in '.ps1', '.psm1' } | Unblock-File; Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force; .\ms-private-GUI.ps1
```
The command unblocks scripts below the current reviewed directory, changes execution policy only for that PowerShell process, and starts the GUI. It does not fetch or execute changing network content.
Complete preset planning and application:
```powershell
.\Preset.ps1 -Mode Plan -Preset Medium
.\Preset.ps1 -Mode Apply -Preset Medium -AcknowledgeDestructivePreset
```
## Validation status
The automated suites cover policy transactions, app identity and sequencing, preset validation, Recall applicability, network recovery, protected paths and services, account redaction, offline-bundle integrity, security audits and AME structure in both Windows PowerShell 5.1 and PowerShell 7. Ten native tests cover payload and manifest integrity, exact-file enforcement, tamper repair, traversal refusal and undeclared archive entries. PE inspection checks architecture, GUI subsystem, UAC manifest, version, checksum and signature state. CI also publishes the one-file executable. GUI startup and read-only host preflights are exercised.
Automated tests establish code behavior against synthetic backends. Production claims still require destructive testing and recovery on the supported Windows edition/build matrix, reboot testing, real Store and Edge recovery, security-update acceptance, sensor hardware checks and controlled network observation.
+14 -4
View File
@@ -2,28 +2,38 @@
## 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.
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.
These checks support a release candidate, not a production-validation claim. Run the matrix below before advertising a stable release. The GitLab pipeline requires a Windows runner tagged `windows`; a remote pipeline result does not replace the live matrix.
The offline-bundle module adds 19 passing synthetic-file tests in PowerShell 7 and 5.1: mismatched hashes and targets, unsafe paths, unknown fields, missing/duplicate packages, dependency ordering, provenance URL validation, invalid dates and reparse-point refusal. These are integrity/validation tests, not real update or network-containment tests. Its host inventory was run read-only. The offline-bundle module adds 19 passing synthetic-file tests in PowerShell 7 and 5.1: mismatched hashes and targets, unsafe paths, unknown fields, missing/duplicate packages, dependency ordering, provenance URL validation, invalid dates and reparse-point refusal. These are integrity/validation tests, not real update or network-containment tests. Its host inventory was run read-only.
The transaction test suite uses an in-memory registry adapter, never production policy writes. It covers edition gates, diagnostic choices, unsupported builds, update modes, microphone exceptions, original value types, read failures, exact restoration, reapply, journal failures, mid-apply failures, writes that fail after mutation, restore conflicts, interrupted operations, repeated restore, untrusted journal entries, installation mismatch and stale plans. The transaction test suite uses an in-memory registry adapter, never production policy writes. It covers edition gates, diagnostic choices, unsupported builds, update modes, microphone exceptions, original value types, read failures, exact restoration, reapply, journal failures, mid-apply failures, writes that fail after mutation, restore conflicts, interrupted operations, repeated restore, untrusted journal entries, installation mismatch and stale plans.
Run `./tests/Run-Tests.ps1` with both PowerShell 7 and Windows PowerShell 5.1. Parse all `.ps1` / `.psm1` files with the PowerShell parser. Run the real host audit read-only and preview app installation using `-WhatIf`. These tests verify software behavior, not Windows policy enforcement. Run `./tests/Run-All.ps1` with both PowerShell 7 and Windows PowerShell 5.1. This includes source parsing and all suites. Run the real host audit read-only and preview app installation using `-WhatIf`. These tests verify software behavior, not Windows policy enforcement.
## VM matrix required before release ## VM matrix required before release
| 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 | Audit reports unsupported machine policies; Apply makes no changes; manual guide matches current Settings |
| Windows 11 Pro 24H2/25H2 | Standard/Strict 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 |
| Domain/MDM managed device | Apply/Restore decline; no competing policies | | Domain/MDM managed device | Apply/Restore decline; no competing policies |
| Non-admin / 32-bit process | Read-only operations work where possible; real mutation declines | | Non-admin / 32-bit process | Read-only operations work where possible; real mutation declines |
| App operations: 19045 / 22631 / 26100 / 26200 | Verify exact selected uninstall scope, app-data behavior, browser installation and reboot. Use a supported servicing arrangement for Windows 10. |
| Edge: supported / restricted removal | Verify successful uninstall where supported; otherwise report failure, preserve WebView2 and permit a revised keep-Edge plan. |
| OneDrive: local / cloud-only / unsynced content | Confirm warnings, normal uninstall, unchanged sync folders and recovery using official installers. |
| WinGet: missing / first-run / offline / cancelled installer | Abort safely; never misreport browser availability or proceed after a failed prerequisite. |
Use snapshot-backed clean VMs, then repeat relevant tests on physical hardware. A VM does not validate real microphones, cameras, TPM behavior or physical network adapters. Never use production credentials in the test matrix. Use snapshot-backed clean VMs, then repeat relevant tests on physical hardware. A VM does not validate real microphones, cameras, TPM behavior or physical network adapters. Never use production credentials in the test matrix.
## Behavior checks ## Behavior checks
After apply and reboot, verify each supported setting in Windows UI and through representative workflows. Confirm camera denial and restoration, packaged microphone denial plus desktop limitations, clipboard sync/history behavior, location-dependent applications, cloud speech and cloud search. Confirm update scan/download/install and Defender updates still work in the normal profile. After apply and reboot, verify each supported setting in Windows UI and through representative workflows. Confirm camera denial and restoration, packaged microphone denial plus desktop limitations, activity upload, app capabilities, clipboard sync/history behavior, location-dependent applications, cloud speech and cloud search. Verify DiagTrack, CDPSvc, CDPUserSvc instances, cloud sync/backup and WER before and after reboot and restore. Confirm update scan/download/install, certificate refresh, Store revocation and Defender updates still work.
Restore original registry types/values and absence; compare with the preflight snapshot. Keep externally changed settings intact and report conflicts. Test interruption before the first write, between writes, during journal replacement and during restore. Test a filled/read-only journal filesystem, registry permission denial and competing changes. Restore original registry types/values and absence; compare with the preflight snapshot. Keep externally changed settings intact and report conflicts. Test interruption before the first write, between writes, during journal replacement and during restore. Test a filled/read-only journal filesystem, registry permission denial and competing changes.
@@ -45,7 +55,7 @@ Verify exact WinGet package ID, publisher download domain, installer version/has
Tor must remain on official update channels with stock fingerprinting protections. Zen and Helium need a current browser-security update check, visible sync/crash/permission decisions and tests that default-browser/profile import remain user-controlled. Tor must remain on official update channels with stock fingerprinting protections. Zen and Helium need a current browser-security update check, visible sync/crash/permission decisions and tests that default-browser/profile import remain user-controlled.
## Data deletion and lock checks for future modules ## Data deletion and lock checks
Require a separate preview for Recall snapshot removal, app data deletion and cleanup. Exclude Downloads/documents by default. Skip locked files, junctions and filesystem reparse points. Never recursively follow arbitrary paths in a cleanup tool. Require a separate preview for Recall snapshot removal, app data deletion and cleanup. Exclude Downloads/documents by default. Skip locked files, junctions and filesystem reparse points. Never recursively follow arbitrary paths in a cleanup tool.
Binary file not shown.
-432
View File
@@ -1,432 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { Workbook, SpreadsheetFile, FileBlob } from '@oai/artifact-tool';
const root = path.resolve(import.meta.dirname, '..');
const out = path.join(root, 'outputs', '01a0b6c2-22ad-7b62-9a5a-4a09014a9b42');
await fs.mkdir(out, {recursive:true});
const reviewed = new Date('2026-09-18T12:00:00Z');
const resumed = new Date('2026-09-19T12:00:00Z');
const S = {
diagnostics:'https://learn.microsoft.com/en-us/windows/privacy/configure-windows-diagnostic-data-in-your-organization',
events:'https://learn.microsoft.com/en-us/windows/privacy/required-diagnostic-events-fields-windows-11-24h2',
optional:'https://learn.microsoft.com/en-us/windows/privacy/optional-diagnostic-data',
services:'https://learn.microsoft.com/en-us/windows/privacy/essential-services-and-connected-experiences',
endpoints:'https://learn.microsoft.com/en-us/windows/privacy/manage-windows-11-endpoints',
cdp:'https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-connectivity#allowconnecteddevices',
gdid:'https://learn.microsoft.com/en-us/windows/deployment/update/wufb-reports-schema-ucdostatus',
reversal:'https://github.com/SmtimesIWndr/gdid-reversal/blob/dd4dd5dfcd5e1af24592ccbce44dac7cf5b38d2d/README.md',
sequel:'https://github.com/SmtimesIWndr/We-running-GDID-back/blob/b4daa443d73433127b59ad6f006eb507072848b3/README.md',
case:'https://www.justice.gov/usao-ndil/media/1450651/dl?inline=',
defender:'https://learn.microsoft.com/en-us/defender-endpoint/cloud-protection-microsoft-antivirus-sample-submission',
smart:'https://learn.microsoft.com/en-us/deployedge/microsoft-edge-security-smartscreen',
edge:'https://learn.microsoft.com/en-us/deployedge/microsoft-edge-policies/urldiagnosticdataenabled',
recall:'https://learn.microsoft.com/en-us/windows/client-management/manage-recall',
ai:'https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-windowsai#disableaidataanalysis',
activity:'https://support.microsoft.com/en-us/windows/privacy/windows-activity-history-and-your-privacy',
search:'https://support.microsoft.com/en-us/windows/privacy/windows-search-and-privacy',
sensors:'https://support.microsoft.com/en-us/windows/privacy/windows-camera-microphone-and-privacy',
location:'https://support.microsoft.com/en-us/windows/privacy/windows-location-service-and-privacy',
storage:'https://support.microsoft.com/en-us/windows/experience/storage-filemanagement/manage-drive-space-with-storage-sense',
ram:'https://veracrypt.io/en/Unencrypted%20Data%20in%20RAM.html',
working:'https://learn.microsoft.com/en-us/windows/win32/memory/working-set',
firewall:'https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/rules',
updates:'https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings',
life:'https://support.microsoft.com/en-us/windows/deployment/updates-lifecycle/windows-10-support-has-ended-on-october-14-2025',
tor:'https://support.torproject.org/tor-browser/security/using-tb-safely/',
vpnTor:'https://support.torproject.org/tor-browser/general/vpn-with-tor/',
zen:'https://zen-browser.app/privacy-policy/',
helium:'https://helium.computer/privacy',
mullvad:'https://mullvad.net/en/help/using-mullvad-vpn-app',
mullvadLogs:'https://mullvad.net/en/help/no-logging-data-policy',
tails:'https://tails.net/about/index.en.html',
appprivacy:'https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy',
winutil:'https://github.com/ChrisTitusTech/winutil',
winutilSupport:'https://github.com/ChrisTitusTech/winutil/blob/main/docs/src/content/docs/guides/getting-started.mdx',
debloat:'https://github.com/Raphire/Win11Debloat/wiki/Features',
privacy:'https://github.com/undergroundwires/privacy.sexy',
sophia:'https://github.com/farag2/Sophia-Script-for-Windows',
sophiapp:'https://github.com/Sophia-Community/SophiApp',
xd:'https://github.com/builtbybel/xd-AntiSpy',
simple:'https://github.com/henrypp/simplewall',
spy:'https://github.com/crazy-max/WindowsSpyBlocker',
legacy:'https://github.com/W4RH4WK/Debloat-Windows-10',
sycnex:'https://github.com/Sycnex/Windows10Debloater',
bleach:'https://github.com/bleachbit/bleachbit',
port:'https://github.com/safing/portmaster',
portfaq:'https://docs.safing.io/portmaster/faq',
atlas:'https://github.com/Atlas-OS/Atlas',
ame:'https://download.amelabs.net/Privacy%2B%20Testing%20Methodology.pdf',
local:'Local project: docs/STATUS.md; config/policies.json; tests/Run-Tests.ps1',
design:'Project engineering decision; not a verified Windows guarantee'
};
// Separate software delivery from host application and observed effectiveness.
const policies = JSON.parse(await fs.readFile(path.join(root,'config/policies.json'),'utf8'));
const controls = policies.map(p => [p.Id, p.Title, 'Windows policy', p.Standard !== null ? 'Default' : p.Strict !== null ? 'Strict' : 'Optional', 'Implemented', 'Not applied', 'Pending',
p.Impact, `HKLM\\${p.Path}\\${p.Name}. Standard=${p.Standard ?? 'unchanged'}; Strict=${p.Strict ?? 'unchanged'}${p.EnterpriseValue !== null ? '; Enterprise/Education='+p.EnterpriseValue : ''}.`,
'Reviewed Windows 11 Pro/Enterprise/Education builds 26100/26200. Home audit only. Windows 10 not validated.',
'Engine tests passed; reboot and effective-policy VM tests pending. Journal restores prior value; no network guarantee.', '', p.Source, reviewed]);
function c(id,name,area,choice,stage,action,limit,test,source='design') {
controls.push([id,name,area,choice,stage,'Not applied','Pending',action,limit,'Edition/build and installed-feature checks required unless stated otherwise.',test,'',S[source]??source,reviewed]);
}
c('tailored','Personalized offers and diagnostic personalization','Diagnostics','Default','Planned','Turn off personalized offers based on diagnostics.','Separate from diagnostic collection; names and availability vary by build.','Verify Settings and effective policy after reboot.','diagnostics');
c('feedback','Feedback prompts','Diagnostics','Optional','Planned','Suppress unsolicited feedback requests.','Does not disable diagnostic transport.','Check supported setting on each edition.','diagnostics');
c('crash-upload','Optional crash and error submissions','Diagnostics','Default','Planned','Reduce optional diagnostic crash uploads; review Windows Error Reporting separately.','Dumps may contain user content. Preserve useful local troubleshooting and security records.','Test app crash handling and retention without uploading real user documents.','optional');
c('edge-url','Edge optional URL diagnostics','Browser','Default','Planned','If Edge remains, disable optional URL/per-page diagnostics using its policy.','This is separate from SmartScreen, browsing sync and Windows diagnostics.','Check edge://policy and a lab traffic baseline; keep browser updates.','edge');
c('recall','Recall snapshot retention','AI','Default','Planned','Detect eligibility; turn off snapshot saving with supported controls.','Some Recall policies delete existing snapshots. Present deletion separately; rollback cannot recover them.','Test eligible hardware/VM where supported; confirm setting and storage outcome.','ai');
c('recall-export','Recall export and Click to Do','AI','Default','Research','Review export settings and Click to Do separately from Recall saving.','Feature, region and edition restrictions apply. Click to Do policy does not control its Recall integration.','Inventory availability and verify each supported control.','recall');
c('copilot','Copilot and application AI features','AI','Optional','Planned','Offer per-app removal and cloud-AI settings for unused features.','One Copilot toggle does not cover Office, Edge, search, Recall and third-party AI.','Inventory exact packages; preserve shared runtimes; test reinstall.','debloat');
c('cdp','Connected Devices Platform','Connected features','Optional','Research','Evaluate documented AllowConnectedDevices deployment; reboot required.','No invented registry mapping. Suppressing CDP has not been shown here to eliminate GDID.','Test supported CSP/management route and cross-device breakage in a VM.','cdp');
c('phone-link','Phone Link and cross-device experiences','Connected features','Optional','Planned','Unlink and uninstall unused Phone Link features through supported interfaces.','Account-side records and other devices remain separate.','Test phone integration removal and restoration.','services');
c('windows-backup','Windows Backup and settings sync','Connected features','Optional','Planned','Disable unwanted account backup and sync after reviewing recovery needs.','Do not silently delete recovery keys or existing backups.','Verify local data availability and supported recovery path.','services');
c('local-account','Local Windows account','Accounts','Optional','Planned','Offer a local account where supported; avoid unnecessary Microsoft sign-in.','A local account is not proof of no device identifier; app sign-ins remain independent.','Preserve account access and recovery before any migration.','design');
c('cloud-records','Existing provider records','Accounts','Optional','Planned','Provide links to account privacy/export/deletion controls.','Local toggles cannot delete provider logs, retained records or other people\'s copies.','Document provider-specific deletion scope and retained categories.','services');
c('activity-history','Local activity history','Retention','Optional','Planned','Stop local activity history and separately offer clear history.','Cloud upload option was deprecated on patched Windows 10/11 in January 2024. Avoid obsolete spying claims.','Check actual build and Settings; distinguish future retention from deletion.','activity');
c('start-web','Start web search and suggestions','Connected features','Optional','Research','Review Start/Bing web queries independently from cloud-source search.','AllowCloudSearch alone does not establish that web queries stop.','Find supported edition-specific policy and test actual queries.','search');
c('widgets','Widgets, weather and news','Connected features','Optional','Planned','Disable or remove unused Widgets through supported controls.','Changes functionality and may affect bundled app packages.','Test update/reinstall and taskbar behavior.','debloat');
c('recommendations','Consumer suggestions and promotional content','Connected features','Default','Planned','Review Start suggestions, Settings offers and Spotlight content individually.','Different settings and edition support; no single total-advertising switch.','Check visible result plus policy after feature updates.','debloat');
c('typing','Typing, inking and personal dictionary','Input','Optional','Research','Reduce online personalization and review local dictionary retention.','Local and cloud settings differ; existing cloud-speech policy covers only one route.','Check language/accessibility impact and supported controls.','appprivacy');
c('app-permissions','Contacts, calendar, messages and account information','Permissions','Default','Planned','Review covered app permissions and deny unused access.','Packaged-app controls do not constrain every desktop program.','Test covered apps and exceptions; do not report hardware isolation.','appprivacy');
c('sensor-hardware','Camera and microphone hardware control','Sensors','Optional','Planned','Offer device-wide settings, physical disconnect or hardware switches.','Software disablement is reversible by privileged software; some cameras support Windows Hello.','Test desktop apps, browsers, conferencing and authentication.','sensors');
c('radio','Bluetooth, nearby sharing and Wi-Fi discovery','Network','Optional','Research','Disable unused discovery and review Wi-Fi MAC randomization support.','May disconnect input devices; MAC changes concern local networks, not global anonymity.','Inventory adapters; test reconnect and recovery without disabling active input.','design');
c('remote','Remote access and file sharing','Network','Optional','Planned','Review unnecessary RDP, Remote Assistance and sharing exposure.','Managed devices and legitimate remote support need separate treatment.','Test reachability, account access and rollback in lab.','design');
c('onedrive','OneDrive removal','Debloat','Optional','Planned','Offer unlink/uninstall after local-copy and folder-redirection preflight.','Cloud-only files must be downloaded or separately backed up first; removal does not delete cloud copies.','Open sampled files offline; verify backup and known-folder paths.','design');
c('edge-removal','Edge removal','Debloat','Optional','Research','Use supported uninstall only where available; otherwise configure privacy.','Keep WebView2 separate. Do not force-delete servicing files to remove the browser.','Validate OS region/build, dependent apps and future security updates.','design');
c('app-removal','Unused consumer and OEM applications','Debloat','Optional','Planned','Inventory and select exact packages; distinguish current-user and provisioned removal.','Keep dependencies, Store/update infrastructure and accessibility tools unless explicitly selected.','Preview removals; test reinstall and new-user provisioning.','design');
c('third-party','OEM, GPU, game-launcher and application analytics','Third parties','Default','Research','Audit each installed app\'s analytics, sync, crash and advertising controls.','Windows settings do not govern all third parties; anti-cheat and drivers need individual review.','Record app/version/publisher and official setting; observe residual connections.','design');
c('office','Office and productivity cloud experiences','Third parties','Optional','Research','Review diagnostics and optional connected experiences in each product.','Account sync, licensing and collaborative features have separate dependencies.','Use product-specific policy docs before implementation.','design');
c('storage-sense','Temporary-file retention','Cleanup','Default','Planned','Use Storage Sense with explicit retention; skip active files.','Exclude Downloads by default; recycle-bin deletion gets its own choice. This is housekeeping, not sanitization.','Preview candidates and exclusions; verify no deletion outside selected directories.','storage');
c('browser-retention','Browser cookies, cache and history','Cleanup','Optional','Planned','Use browser-owned retention and clear-on-exit options.','Clearing cookies signs users out; downloaded files, sync and server records remain.','Test each browser; preserve passwords/bookmarks unless separately selected.','design');
c('recent-files','Recent items, thumbnails and application MRUs','Cleanup','Optional','Research','Minimize retention and offer scoped, app-specific cleanup.','Deleting caches is irreversible and they can reappear. No sweeping registry cleanup.','Inventory selected cache schema and test with disposable data.','design');
c('temp-script','Scoped temporary-folder cleanup fallback','Cleanup','Optional','Planned','If needed, build preview-first cleaner with age threshold and literal allowed roots.','Skip junctions/reparse points, locked files and active installer data; no recursive arbitrary paths.','Tests must prove no traversal outside approved directories.','design');
c('registry-cleaner','Registry cleaner','Cleanup','Reject','Excluded','Do not include a general registry cleaner.','Unrelated registry deletion has no demonstrated identifier-blocking benefit here and can break software.','Use exact documented setting changes with a recovery journal.','design');
c('no-logs','Blanket Windows log deletion','Retention','Reject','Excluded','Retain security, update and recovery logs; minimize toolkit data collection instead.','No-logs is not a credible OS-wide promise; disabling records harms diagnosis and incident response.','Ensure toolkit contains no analytics, browsing data, keys or identifiers.','design');
c('ssd-wipe','Routine SSD free-space wiping','Cleanup','Reject','Excluded','Do not use as routine privacy cleanup.','Wear, remapped blocks, backups and prior copies defeat simple erasure claims.','Use appropriate device sanitization procedures only for a separate disposal task.','design');
c('ram-cleaner','RAM trimming and generic memory cleaners','Memory','Reject','Excluded','Do not include as an anonymization feature.','Working-set reduction is not proof that sensitive memory was erased.','Use application lock/unmount and protected storage.','working');
c('lock-actions','Actions on session lock','Memory','Optional','Planned','Lock password manager, clear clipboard and gracefully unmount selected non-system containers.','Win+L does not erase all keys, tokens or keystrokes. Do not kill LSASS or arbitrary apps.','Test every session-lock path, busy volumes, unsaved work and visible failure on unlock.','ram');
c('sleep-shutdown','Sleep, hibernation and shutdown choices','Memory','Optional','Research','Explain that a locked running OS can retain secrets; offer an intentional full-shutdown workflow.','Shutdown is not a guarantee of instantaneous physical RAM erasure; hibernation uses storage.','Test Fast Startup/hibernation behavior and preserve encryption/recovery.','ram');
c('disk-encryption','System disk encryption','Security','Keep','Planned','Keep or enable supported system encryption and review recovery-key custody.','Do not disable BitLocker/device encryption as a debloat tweak. Never collect recovery secrets in reports.','Verify encryption status and recovery access without logging keys.','design');
c('veracrypt','VeraCrypt containers','Applications','Optional','Prepared','Optional interactive installer is ready; containers and lock integration remain manual.','Installed does not mean encrypted; mounted data and other apps can retain plaintext.','Verify package publisher; create/test disposable container and graceful unmount.','ram');
c('tor-browser','Tor Browser','Applications','Optional','Prepared','Installer prepared; use stock protections for sessions requiring unlinkability.','Protects its configured browser traffic; personal logins identify a session.','Verify official release, connection and no profile import or added extensions.','tor');
c('zen-browser','Zen','Applications','Optional','Prepared','Installer prepared for everyday Firefox-based browsing.','Privacy policy acknowledges external requests; settings and releases require review.','Test updates, permissions, sync choices and retention.','zen');
c('helium-browser','Helium','Applications','Optional','Prepared','Installer prepared for everyday Chromium-based browsing.','Optional crash reports and configured services are separate privacy decisions.','Test updates, extension services, permissions and optional reporting.','helium');
c('mullvad-vpn','Mullvad VPN','Applications','Optional','Prepared','Installer prepared; add funded account, auto-connect and chosen Lockdown behavior.','Installation is not connection. VPN changes network observer and egress IP, not application identifiers.','Test disconnect, DNS, IPv6, reboot, sleep/wake and split-tunnel exceptions.','mullvad');
c('vpn-logs','VPN logging claims','Network','Keep','Research','Describe provider activity-logging policy separately from local app diagnostics.','A provider no-logging policy is not a no-logs Windows guarantee.','Review current policy and local support-bundle contents before sharing.','mullvadLogs');
c('tor-vpn','Automatic VPN plus Tor stacking','Network','Reject','Excluded','Do not turn on this combination by default.','Tor warns against casual VPN combinations; threat model and configuration matter.','Offer separate documented workflows if a user needs the combination.','vpnTor');
c('dns-blocking','DNS tracker blocking','Network','Optional','Planned','Use a reviewed resolver/filter with narrow lists and a clear logging policy.','Direct IP, alternate resolvers and shared domains limit coverage; DNS blocking is not all egress control.','Test false positives and IPv4/IPv6/DoH behavior; version lists and rollback.','design');
c('egress','Application egress control','Network','Optional','Research','Evaluate one network enforcement tool in a VM; start from measured workflows.','Keep recovery access; do not stack competing WFP tools blindly.','Test VPN coexistence, update paths, reconnect, reboot and uninstallation.','simple');
c('denylist','Microsoft endpoint denylist','Network','Optional','Research','Classify optional endpoints by feature, owner, purpose and capture evidence before blocking.','Do not block all Microsoft/Azure/CDN IPs or svchost.exe. Published lists are edition/version-specific.','Prove both reduced traffic and successful security workflows.','endpoints');
c('outbound-default','Strict outbound default policy','Network','Optional','Research','For an advanced profile, use default-block with reviewed required allowances.','Explicit block rules override conflicting allows. Existing allows and all network profiles must be audited.','Disposable VM first; timed recovery and firewall-export rollback.','firewall');
c('defender','Antivirus and cloud-delivered protection','Security','Keep','Planned','Keep active antivirus, tamper protection and cloud protection in the standard design.','Some security metadata leaves the device; this is an explicit retained security tradeoff.','Verify provider status, signatures and vendor-supported protection checks.','defender');
c('sample-consent','Antivirus file sample submission','Security','Optional','Research','Expose supported submission choices with their security cost.','Turning samples off can impair block-at-first-sight; metadata can still be sent.','Test exact edition/product behavior. Never label samples-off as cloud-off.','defender');
c('smartscreen','SmartScreen and reputation checks','Security','Keep','Planned','Keep reputation protection in the standard design.','Security URL checks differ from optional URL diagnostics and browser sync.','Use vendor demonstration checks, not live malware; test after network restrictions.','smart');
c('trust','Certificate trust, revocation and time','Security','Keep','Planned','Preserve certificate trust/revocation, time sync, activation and servicing dependencies.','An endpoint may support essential security; broad domain blocking risks silent failures.','Verify TLS trust and OS servicing after restrictions.','services');
c('patching','Security update workflow','Security','Keep','Planned','Default to current update behavior; optional notify/manual choice remains visible.','Manual has missed-patch risk. Do not remove servicing components or disable all update services.','Install actual cumulative update and AV/browser updates; reboot and rescan.','updates');
c('windows10','Windows 10 coverage','Security','Keep','Research','Require supported edition or verified ESU enrollment before deployment.','Standard support ended 2025-10-14. Consumer page currently lists 2027-10-12; verify region/program at release.','Record exact SKU, coverage and installed security update. Windows 10 engine support is pending.','life');
c('boot-hardening','Secure Boot, TPM, VBS and exploit mitigations','Security','Keep','Planned','Preserve supported hardware and OS security protections.','Do not trade them for a privacy or performance preset; firmware updates remain necessary.','Inventory status and compatibility before any optional change.','design');
c('third-party-av','Bitdefender or Zillya','Security','Optional','Research','Do not replace working Defender automatically; evaluate a chosen product separately.','This project has not established a comparative privacy/security advantage or tested these vendors.','Review current privacy policy, independent protection tests, update support and uninstall behavior.','design');
c('identifier-spoof','GDID, MachineGuid and hardware-ID spoofing','Identifiers','Reject','Excluded','Do not ship undocumented identifier edits or a universal ID reset.','Changing one value cannot prove unlinkability and may break licensing or apps.','Investigate mechanisms in a disposable lab without exporting actual personal identifiers.','design');
c('tails','Separate amnesic environment','Isolation','Optional','Planned','Document Tails USB for a separate session with explicit persistence choice.','Does not protect against malicious hardware or voluntary identity disclosure.','Verify official image and usable hardware/network before relying on it.','tails');
c('identity-habits','Accounts, posts and shared metadata','Behavior','Default','Planned','Reduce unnecessary disclosure; separate contexts and review document/photo metadata before publishing.','Reused accounts, shared content and personal details can reconnect sessions despite device settings.','Use a practical publishing checklist; never promise technical anonymity.','design');
c('tool-retention','Toolkit data minimization','Toolkit','Default','Implemented','No analytics added; local settings journal supports restoration.','A recovery journal exists, so the tool is not literally log-free. Audit deliberately avoids GDID and recovery keys.','Review code and generated audit; document optional journal removal after recovery window.','local');
const identifiers = [
['GDID / GlobalDeviceId','Installation/device linkage','Microsoft reports and case records','Do not equate with advertising ID. No verified universal reset control here.','Investigate feature paths and effective network reductions in lab.','Documented field; collection path incomplete',S.gdid],
['PCFP','Hardware-derived identifier','Windows 11 required diagnostic schema','Documented as an identifier calculated from hardware identifiers.','Use supported diagnostic minimum; do not assume reinstall removes hardware correlation.','Documented schema; emission not measured',S.events],
['CensusId','Hardware identifier','Appraiser RunContext schema','Documented hardware identifier in compatibility-related events.','Preserve compatibility/update testing; no verified per-field suppression in this project.','Documented schema; emission not measured',S.events],
['Advertising ID','Advertising personalization','Windows and participating applications','One specific advertising identifier; disabling it does not disable other IDs.','Implemented catalog control; not applied to host.','Official policy',policies.find(p=>p.Id==='advertising-id').Source],
['Microsoft account / app accounts','Account identity','Signed-in services','Local Windows account does not sign every application out.','Minimize unnecessary sign-in and sync; preserve recovery access.','Architecture / design assessment',S.services],
['MachineGuid / serials / TPM identity','Local and service-specific identifiers','OS, firmware and application-dependent','No evidence here that one registry edit removes all remote identification.','Avoid collecting them; no spoofing feature.','Scope limitation, not a measured traffic claim',S.design],
['Public IP / timestamps','Network correlation','Destination services and network intermediaries','VPN changes visible egress but not IDs inside app traffic.','Mullvad configuration and leak tests; minimize account linkage.','Network design assessment',S.mullvad],
['Wi-Fi MAC','Local radio/network identity','Nearby Wi-Fi infrastructure','Not normally the same identifier as a remote website sees.','Evaluate supported MAC randomization; retain network compatibility.','Design item; adapter testing pending',S.design],
['Cookies / browser storage / fingerprint','Web session linkage','Websites and embedded third parties','Multiple browsers do not automatically separate identities.','Tor stock protections; appropriate retention and account separation.','Browser threat-model guidance',S.tor],
['Optional browser URL diagnostics','Browsing data','Optional Microsoft browser diagnostics','Windows optional-data documentation includes Microsoft browser activity; not proof for every browser.','Disable optional collection; Edge URL policy separately.','Official documentation',S.optional],
['Crash dumps','Potential memory contents','Diagnostic collection and local storage','Optional crash reports can contain portions of documents or pages.','Minimize uploads and retention without discarding all troubleshooting logs.','Official documentation',S.diagnostics],
['Defender security metadata','File, process and network context','Cloud protection services','Metadata may still be sent with sample submission disabled.','Keep standard protection; explain data flow and optional tradeoff.','Official documentation',S.defender],
['Recall snapshots','Local visual history / optional export','Eligible Recall installations','Distinct from Copilot chat; export and retention have separate controls.','Disable unused snapshot saving; preview irreversible deletion.','Official documentation',S.recall],
['Activity history','Local activity record','Supported Windows feature','Patched Windows deprecated its cloud-upload option in January 2024.','Control local retention; avoid repeating outdated upload claims.','Official documentation',S.activity],
['Third-party app identifiers','Product-specific telemetry','OEM utilities, browsers, launchers and apps','Requires app-specific evidence; no blanket Windows toggle.','Inventory publishers/settings and measure each application.','Research task',S.design]
].map((r,i)=>[`I${String(i+1).padStart(2,'0')}`,...r.slice(0,6),'',r[6],reviewed]);
const toolsData = [
['Chris Titus WinUtil','Tweaks and app deployment','Current docs: Windows 11; Windows 10 unsupported','Useful UI and selective settings reference.','Inspect exact tweaks; exclude security/BitLocker reductions and blanket presets.','Candidate reference; not executed',S.winutil,S.winutilSupport],
['Raphire Win11Debloat','Selective debloat and settings','Windows 10/11 project; exact build still needs testing','Per-app selection, previews and current feature catalog.','Exclude encryption-disabling choices; distinguish default restoration from original-state rollback.','Candidate for selected debloat; not executed','https://github.com/Raphire/Win11Debloat',S.debloat],
['privacy.sexy','Generated privacy scripts','Windows catalog; validate each script/build','Source-linked scripts and individual selection help review.','Do not select all scripts. Review reversals and license before code reuse.','Reference library; not executed',S.privacy,S.privacy],
['Sophia Script','PowerShell customization','Version-specific Windows 10/11 variants','Function-by-function design and corresponding default functions.','Default functions are not proof of restoring every original user value.','Selective reference; not executed',S.sophia,S.sophia],
['SophiApp','Graphical tweak tool','Repository warns about deprecated 1.0.97','Related GUI project; inspect current supported replacement.','Do not deploy deprecated build based on a promised future version.','Legacy build excluded; recheck release',S.sophiapp,S.sophiapp],
['xd-AntiSpy','Privacy/settings UI','Windows 11 focus; Windows 10 partial','Readable choices and plugin-based configuration.','Plugins expand trust surface; check exact changes and reversals.','Candidate for review; not executed',S.xd,S.xd],
['simplewall','WFP application filtering','Windows 10/11 included','Useful per-app egress enforcement candidate.','Its filters can remain after uninstall unless removed/disabled. Test temporary filtering and recovery.','Lab candidate; not installed',S.simple,S.simple],
['WindowsSpyBlocker','Traffic research and blocklists','Windows-oriented; current endpoint validation required','Traffic classification is useful input for lab analysis.','Treat lists as hypotheses, not permanent safe blocks. Separate update dependencies.','Research input; no rules imported',S.spy,'https://crazymax.dev/WindowsSpyBlocker/'],
['Portmaster','Application firewall and DNS filtering','Windows/Linux; verify current installer compatibility','Alternative to simplewall for evaluation.','WFP/VPN coexistence and uninstall connectivity need testing. Avoid stacking both by default.','Alternative lab candidate; not installed',S.port,S.portfaq],
['BleachBit','Local cleanup','Windows/Linux','Preview and app-specific cleaners can support optional retention work.','Deletion is irreversible; no secure-SSD or internet anonymity guarantee.','Optional cleanup candidate; not run',S.bleach,S.bleach],
['W4RH4WK Debloat-Windows-10','Legacy debloat scripts','Archived 2025-09-23; deprecated','Historical examples only.','Not a maintained deployment choice for current Windows.','Excluded',S.legacy,S.legacy],
['Sycnex Windows10Debloater','Legacy Windows 10 debloat','Windows 10 project; Windows 11 not established here','Historical app-removal patterns.','Do not infer current build support or exact-original restoration.','Excluded from current deployment',S.sycnex,S.sycnex],
['Atlas','Windows modification playbook','Version-specific Windows deployment','Reviewable playbook architecture.','Broad OS changes exceed a small privacy profile; test each release rather than rely on old reputation.','Comparison only; not applied',S.atlas,S.atlas],
['AME / Privacy+','Playbook delivery / privacy changes','Depends on playbook and target build','AME could eventually package the reviewed Wind engine.','Privacy+ methodology includes Windows Update removal, conflicting with this project\'s patching requirement.','Do not adopt Privacy+ wholesale',S.ame,S.ame],
['gdid-reversal','Independent reverse-engineering research','Reported observations; not reproduced here','Suggests identity/CDP/Delivery Optimization investigation paths.','Not a validated anonymizer. Separate static findings, observations and hypotheses.','Research lead only',S.reversal,S.reversal]
].map((r,i)=>[`T${String(i+1).padStart(2,'0')}`,...r.slice(0,6),'',...r.slice(6),reviewed]);
// Case-derived text is deliberately short and attributed, not asserted as a universal OS property.
const claims = [
['Microsoft tracked Stokes through GDID','Supported as allegation','Complaint associates the identifier with attributed activity.','Affidavit paragraphs 25–29; this is not a conviction.',S.case],
['Assigned per install; survives updates; changes on reinstall','Attributed description','The affidavit attributes this lifecycle to a Microsoft representative.','Paragraph 25. Do not infer reinstall guarantees unlinkability.',S.case],
['Unique to physical device','Overstated','The description includes physical and virtual Windows installations.','Installation identity and immutable hardware identity differ.',S.case],
['Ngrok, URLs, IPs and timestamps','Supported for this case','The complaint describes these associations.','Paragraphs 26–28 do not specify every collection component.',S.case],
['It records every game played','Overstated','Growtopia login activity was correlated with Ubisoft records.','Paragraph 29 does not establish a complete installed-game history.',S.case],
['Only documented in one obscure page','Unsupported absolute','GlobalDeviceId appears in published Microsoft reporting schemas.','Documentation of a field is not a complete explanation of collection.',S.gdid],
['UCDOStatus proves universal web-history collection','Not established','UCDOStatus is a Delivery Optimization reporting schema.','A named field does not prove that this table records all URLs.',S.gdid],
['Every browser on every Windows PC reports every URL','Not established','Neither cited source demonstrates this universal claim.','Do not present it as the blog\'s conclusion.',S.case],
['Local account guarantees no Microsoft identifier','Not established','Independent research reports a path without Microsoft-account sign-in.','Useful lead; not independently reproduced by this project.',S.reversal],
['GDID is the only persistent identifier worth checking','False simplification','Microsoft also documents PCFP and CensusId in required diagnostic schemas.','Documented fields do not prove emission on this host.',S.events],
['Disabling advertising ID blocks GDID','Not established','The advertising-ID policy governs a different identifier.','Use exact scope labels and lab measurements.',policies.find(p=>p.Id==='advertising-id').Source],
['No telemetry toggle means no Microsoft traffic','False simplification','Essential services and connected experiences have separate data flows.','Preserve agreed security dependencies and document residual traffic.',S.services],
['Win+L erases all secrets from memory','Unsupported','Locking is not a general memory-erasure mechanism.','Plan app-specific locking and graceful unmount; do not claim total erasure.',S.ram],
['Humans can identify themselves despite privacy tools','Design principle','Accounts, posts and shared content can reconnect contexts.','A practical privacy plan includes disclosure habits.',S.design]
].map((r,i)=>[`C${String(i+1).padStart(2,'0')}`,...r.slice(0,4),'',r[4],reviewed]);
const roadmap = [
['P0','VM support matrix','Pro, Enterprise, Education and Home; reviewed Windows 11 builds; separate Windows 10 ESU track','Effective setting after reboot and update, not just registry readback.','Policy behavior','Open'],
['P0','Security preservation baseline','Record antivirus, firewall, boot/encryption and update status without secrets','Cumulative update installs; AV signatures and browser updates succeed; protection checks pass.','Security','Open'],
['P0','Network baseline','Disposable VM, documented workflows, Windows build, network path and capture duration','Compare before/after idle, reboot, browsing, sync and update traffic; no universal absence claim.','Network','Open'],
['P0','Endpoint provenance register','Process/service attribution, hostname/IP, purpose, evidence, expiry and rollback','No unknown destination becomes a default block; validate DNS/IPv6/VPN behavior.','Network','Open'],
['P0','Recoverability of networking','Local recovery access and exported firewall configuration','Timed recovery and uninstall restore connectivity; explicit block precedence tested.','Network','Open'],
['P0','Original-state recovery engine','Existing mock and file journal tests; real Windows registry test in disposable VM','Crash/interruption, conflicting external changes and restoration verified.','Engine','Open'],
['P1','Home Settings workflow','Current host is Core/Home build 26200','Guide accurately maps real Settings; no unsupported policy-enforcement labels.','Compatibility','Open'],
['P1','Sensor verification','Camera/mic devices and covered vs desktop app permissions','Conferencing, browser access and Windows Hello tested; hardware state labelled separately.','Sensors','Open'],
['P1','AI and debloat inventory','Package dependencies and Recall eligibility','Data-deletion preview, OneDrive offline-copy checks and reinstall path pass.','Apps','Open'],
['P1','Retention implementation','Storage Sense first; browser/app-owned cleanup; scoped fallback only','Exclusions, age cutoff, reparse-point protection and active-file handling tested.','Cleanup','Open'],
['P1','Lock-session integration','Password manager adapters and selected non-system volumes','Unsaved work protected; busy-volume failure reported; no system-process killing.','Memory','Open'],
['P1','Identifier investigation','Documented fields plus independent GDID/CDP research','Reproducible lab notes; no claim of a suppression mechanism without measured evidence.','Research','Open'],
['P1','Third-party application register','Installed publishers, versions, controls and official privacy sources','Each app has separate collection/update decisions; no silent global disablement.','Research','Open'],
['P1','Pin upstream artifacts','Selected repository commit, release signature/hash and license review','No download-and-execute one-liners; reviewed local content only.','Supply chain','Open'],
['P1','Browser and VPN acceptance','Tor, Zen, Helium, Mullvad selected individually','Update, publisher, profile separation and leak/Lockdown tests pass.','Applications','Open'],
['P2','Graphical wizard / AME wrapper','Use same reviewed engine and recovery model','UI shows intended change, compatibility, deletion and restoration limits.','Delivery','Open'],
['P2','Blog publication check','Re-read primary case and current product documentation','Claims retain attribution; screenshots and logs contain no private identifiers.','Editorial','Open'],
['P2','Revalidation after Windows updates','Repeat on feature/security updates and changed app releases','Record build/version, deviations and regression outcomes before widening support.','Maintenance','Open']
].map((r,i)=>[`R${String(i+1).padStart(2,'0')}`,...r,'Unassigned',null,'']);
const delivered = [
['D01','Policy catalog','Implemented',13,'config/policies.json','Includes two optional update entries; Standard selects 10, Strict 11.','Not applied'],
['D02','Audit / plan / apply / restore engine','Implemented',1,'Wind.ps1; src/Wind.Core.psm1','Edition/build gates, journal and conflict-aware restoration.','Not applied'],
['D03','Transaction and recovery tests','Passed',23,'tests/Run-Tests.ps1; docs/STATUS.md','Same suite passed in PowerShell 7.6.5 and Windows PowerShell 5.1. Mock registry plus file journal checks.','Not an OS enforcement test'],
['D04','Host read-only audit','Completed',1,'reports/host-audit.json','Core/Home build 26200. Automatic policy enforcement declined. No private IDs collected.','Read only'],
['D05','Interactive app choices','Prepared',5,'config/apps.json; Install-Apps.ps1','Tor, Zen, Helium, Mullvad, VeraCrypt. Installer previews only.','Not installed'],
['D06','Research, Home guide and validation plan','Prepared',1,'docs/RESEARCH.md; docs/HOME-GUIDE.md; docs/VALIDATION.md','Implementation scope and known limits documented.','Documents only'],
['D07','GitHub comparison and control workbook','Prepared',1,'Windows-privacy-register.xlsx','Sources reviewed 2026-09-18; repository execution and lab validation remain pending.','Documents only'],
['D08','Blog draft','Prepared',1,'Windows-privacy-blog-draft.md','Evidence, motivation, privacy choices and honest completion status.','Not published']
];
// Resume additions retain previous evidence dates and identify newly reviewed material.
for (const [id,name,action,limit,test,src] of [
['cdp-experiment','Experimental CDP restrictions','Investigate CDP independently from other identity consumers.','Supplied script is not integration-ready; review records status and rollback defects.','Reproduce effects in disposable Windows environments; no host deployment.','sequel'],
['wns-review','WNS notification blocking','Keep general WNS blocking out of the default design.','Notifications and management may be affected; a hosts entry is not a complete egress block.','Verify exact consumer and retained security functionality before any narrow restriction.','endpoints'],
['store-id-review','Store identifier path','Investigate Store requests separately; preserve app security updates.','Do not block the entire Store or edit its identifier based on one report.','Lab observation with disposable accounts and redacted evidence.','sequel']
]) { c(id,name,'Identifiers','Optional','Research',action,limit,test,src);controls.at(-1)[13]=resumed; }
identifiers.push(
['I16','Multiple device PUIDs','Context-specific identity','Independent follow-up research','Author reports several device PUIDs on one installation.','Extend lab scope beyond the interactive user.','Reported; not reproduced here','',S.sequel,resumed],
['I17','Widgets identifier emission','Additional application path','Independent follow-up research','Author reports Widgets sending a SYSTEM-context identifier.','Test Widgets independently from CDP.','Reported; not reproduced here','',S.sequel,resumed],
['I18','SQMClient MachineID / Store','Separate identifier family','Independent follow-up research','Author reports a matching Store request identifier.','Preserve Store updates; investigate separately.','Reported; not reproduced here','',S.sequel,resumed]
);
toolsData.push(['T16','We-running-GDID-back','Follow-up identifier research','Pinned b4daa443; Windows 11 observations','Expands the investigation beyond one CDP identifier.','Client symbols and token scopes alone do not prove backend reconciliation or every transmission.','Research lead only','',S.sequel,S.sequel,resumed]);
claims[1][4]='Paragraph 25 attribution; follow-up research raises additional lifecycle/context questions.';
claims.push(
['C15','One identifier per installation is the complete model','Incomplete','Follow-up research reports multiple device PUIDs.','Treat as a lab hypothesis; retain court attribution separately.','',S.sequel,resumed],
['C16','Disabling CDP proves all device IDs stopped','Not established','No test here establishes complete identifier suppression.','Test each emitting feature and retained service separately.','',S.sequel,resumed],
['C17','An ETW HTTPS URL means cleartext on the wire','Incorrect inference','Local instrumentation can reveal data before transport encryption.','Differentiate local visibility, destination visibility and network-observer visibility.','',S.design,resumed]
);
roadmap.push(
['R19','P0','Replace unsafe pasted-script patterns','See Script review findings and pinned source revisions','No integration until exact-state rollback, honest status and independent cleanup pass tests.','Engine','Open','Unassigned',null,''],
['R20','P1','Reproduce follow-up identifier findings','Disposable accounts; context-specific identifiers; Widgets and Store independently','Record observed flows without raw IDs/tokens in reports; retain update/security acceptance tests.','Research','Open','Unassigned',null,'']
);
delivered.push(['D09','Pasted script and follow-up research','Reviewed',1,'docs/GDID-SCRIPT-REVIEW.md','Static review, parse check and three in-memory defect probes. No supplied script executed.','Read only']);
const scriptReview = [
['S01','High','False status result','24–25; 120','Writes GDID-BLOCKER but searches GDID-BLOCK.','Match constants exactly; verify entries and effective behavior.','Confirmed in-memory'],
['S02','High','False success after partial failure','19; 49–64; 132; 138','Continues after errors and ignores native exit codes.','Check every result and effective service state; report partial failures.','Static finding'],
['S03','High','Policy rollback destroys prior state','68; 72','Overwrites three values then deletes them on enable.','Journal original presence/type/value and detect conflicts.','Static finding'],
['S04','High','Per-user service template handling','44; 49; 52','Template inventory differs from generated service instances.','Use documented template handling; test sign-in and reboot.','Official docs + static review'],
['S05','Medium','Incomplete service restoration','44; 57–64','Does not preserve delayed-start metadata or running state.','Record exact state; do not guess Manual or always start.','Static finding'],
['S06','High','Nontransactional hosts edits','76–97','Backup is not used by enable; whole file is rewritten.','Preserve bytes/encoding, journal, atomically replace and detect concurrent edits.','Static finding'],
['S07','High','Malformed marker removes other entries','79–84; 90–95','Missing end marker drops every following line.','Reject unmatched or duplicate markers before mutation.','Confirmed in-memory'],
['S08','Medium','ASCII conversion changes content','86; 96','Non-ASCII comments do not survive.','Preserve detected encoding and original bytes.','Confirmed in-memory'],
['S09','High','Irreversible cache deletion / user scope','100–105','Targets elevated LOCALAPPDATA; enable cannot restore deleted data.','Separate explicit deletion; resolve intended profile and reject path escapes.','Static finding'],
['S10','High','Hosts list overclaims suppression','30–38','Exact names do not prove all paths or identifiers are blocked.','Measured per-feature controls and residual-traffic reporting.','Design limitation'],
['S11','Medium','Notification side effects','34–37','WNS is shared infrastructure, not a GDID-only endpoint.','Keep default notifications intact pending narrow tests.','Official endpoint docs'],
['S12','Medium','Status elevates and creates state','11–27','Read-only mode enters the mutation setup path.','Audit without elevation or filesystem writes.','Static finding']
].map(r=>[...r,'Not integrated','',r[0]==='S04'?'https://learn.microsoft.com/en-us/windows/application-management/per-user-services-in-windows':r[0]==='S11'?S.endpoints:'User attachment Pasted text.txt; SHA256 0603A1073072CCC55B78692ACC93FAED4363CCFC0AF700472FBC862788B97E13',resumed]);
const U={
checkpoint:'https://learn.microsoft.com/en-us/windows/deployment/update/catalog-checkpoint-cumulative-updates',
scan:'https://learn.microsoft.com/en-us/windows/win32/wua_sdk/using-wua-to-scan-for-updates-offline',
av:'https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-updates',
trust:'https://learn.microsoft.com/en-us/windows-server/identity/ad-cs/configure-trusted-roots-disallowed-certificates',
ncsi:'https://learn.microsoft.com/en-us/windows-server/networking/ncsi/ncsi-overview',
wer:'https://learn.microsoft.com/en-us/windows/win32/wer/windows-error-reporting',
local:'Local project: Offline-Updates.ps1; src/Wind.OfflineUpdates.psm1; docs/OFFLINE-UPDATES.md'
};
for(const [id,name,area,choice,stage,action,limit,test,source]of[
['offline-verifier','Offline bundle integrity verifier','Update architecture','Optional','Implemented','Verify local manifest, hashes, target build/architecture and declared dependency order.','Authenticity, applicability, coverage and containment remain unverified. No downloads or installation.','19 synthetic-package tests passed in PowerShell 7 and 5.1; real host inventory read.',U.local],
['update-collector','Separate update collector','Update architecture','Optional','Planned','Download generic publisher packages on another machine for controlled transfer.','Collector still contacts publishers. Do not forward protected-PC requests or credentials.','Authenticate sources, packages and current revocation state; test transfer and supply chain.',U.checkpoint],
['offline-scan','Offline Windows Update Agent scan','Update architecture','Optional','Planned','Use a fresh signed scan CAB and explicit offline search in an isolated lab.','Scan catalog is metadata, not update binaries or complete third-party coverage.','Verify temporary service cleanup, freshness, missing-update scope and external isolation.',U.scan],
['isolation','No direct Internet from protected Windows','Update architecture','Optional','Research','Use a disconnected Windows environment and a separate maintenance/browsing environment.','Online cloud protection and reputation queries are unavailable. A denylist is not proof of isolation.','No attached VM NIC or physical disconnection; independent verification and local recovery.',S.design],
['ncsi','Connectivity probes','Connected features','Optional','Research','Review NCSI; consider supported private probes on managed networks.','Turning off probes can affect perceived connectivity and applications.','Test captive portals, network changes and update behavior.',U.ncsi],
['offline-trust','Offline certificate trust maintenance','Update architecture','Keep','Planned','Provide trusted/disallowed certificate-list maintenance for the isolated profile.','Never disable certificate validation; offline revocation knowledge can become stale.','Verify provenance, freshness and package trust after transfer.',U.trust],
['patch-coverage','Whole-system patch coverage','Update architecture','Keep','Planned','Track Windows, .NET, Defender, browsers, firmware, drivers and recovery separately.','Current OS build or a zero-result scan does not establish full patch coverage.','Record actual installed versions, reboot outcomes and emergency-update process.',U.av],
['local-discovery','Local-network identity disclosure','Local network','Optional','Research','Inventory hostname discovery, LLMNR/mDNS, NetBIOS and sharing.','These are not automatically Microsoft-bound flows; disabling discovery can break local devices.','Use disposable network captures and test printers/sharing before restrictions.',S.design],
['credential-exclusion','Exclude credential stores from cleanup','Cleanup','Default','Planned','Keep identity caches, login tokens, recovery material and servicing stores out of generic cleanup.','Deleting a cache does not establish deletion of remote records and may break login or updates.','Verify exclusions and exact allowed roots before implementing cleanup.',S.design]
]) {c(id,name,area,choice,stage,action,limit,test,source);controls.at(-1)[13]=resumed;}
for(const [id,name,purpose,platform,use,limit,decision,url]of[
['T17','Win-Debloat-Tools','Legacy debloat','Archived 2025-10-03; last documented builds limited','Historical reference.','Removal may not be easily reversed; not a current supported deployment base.','Excluded','https://github.com/LeDragoX/Win-Debloat-Tools'],
['T18','Harden-Windows-Security','Security hardening','Current Windows security focus; exact app requirements need review','Individual security controls can inform preservation tests.','Connected-service recommendations are not our privacy profile; do not import all defaults.','Selective reference only','https://github.com/HotCakeX/Harden-Windows-Security'],
['T19','wumgr','Update management','Project described for Windows 10','Update-management UI comparison.','Does not create a no-contact boundary. Current Windows 11/ESU behavior unvalidated.','Evaluation only','https://github.com/DavidXanatos/wumgr']
])toolsData.push([id,name,purpose,platform,use,limit,decision,'',url,url,resumed]);
identifiers.push(
['I19','Connectivity probes','IP / timing at probe destination','Microsoft NCSI documentation','Connectivity detection is another independent network function.','Optional control with compatibility tests.','Documented; host traffic not captured','',U.ncsi,resumed],
['I20','Update request metadata','Requests / network metadata','Online update architecture','Direct downloads need outbound protocol traffic.','Separate collector for no direct protected-PC contact.','Protocol constraint / design','',U.checkpoint,resumed],
['I21','Windows Error Reporting','Fault reporting','Microsoft WER documentation','Separate from third-party application reporters.','Review reporting and local retention individually.','Documented; host traffic not captured','',U.wer,resumed]
);
claims.push(
['C18','Download directly from Microsoft while sending it nothing','Not technically achievable','Requests and transport traffic are needed for a direct download.','Use a separate collector if the protected PC must not contact Microsoft.','',S.design,resumed],
['C19','A proxy or VPN removes identifiers from update requests','Not established','Relaying a request does not necessarily alter its contents.','Only a reviewed design that serves pre-fetched content avoids forwarding those requests.','',S.design,resumed],
['C20','Offline patches retain every online security capability','Incorrect','Online cloud analysis and reputation checks require connectivity.','Preserve local protections and explain the cloud-security tradeoff.','',S.defender,resumed]
);
roadmap.push(
['R21','P0','Validate offline update end to end','Separate collector; authentic packages; isolated target; supported edition','Install known applicable updates and prerequisites; reboot and rescan; verify no target WAN access.','Updates','Open','Unassigned',null,''],
['R22','P0','Implement offline WUA scanner','Signed current catalog, explicit offline search, temporary service registration','No online search or fallback; cleanup on failure; no full-coverage claim from empty results.','Updates','Open','Unassigned',null,''],
['R23','P0','Offline security-maintenance coverage','Defender platform/engine/intelligence, trust lists, apps and firmware','Each required component has a tested source, freshness policy and installation evidence.','Security','Open','Unassigned',null,''],
['R24','P1','Collector authenticity and transfer workflow','Publisher signatures and package catalogs; protect manifest and files','Hashes never stand in for publisher identity; reverify immediately before execution.','Supply chain','Open','Unassigned',null,'']
);
delivered.push(['D10','Offline bundle verifier and OS inventory','Implemented',1,'Offline-Updates.ps1; src/Wind.OfflineUpdates.psm1','19 tests passed in PowerShell 7 and 5.1. No download, scan, installation or isolation applied.','Read only']);
delivered[6][5]='Sources reviewed 2026-09-18/19; execution and lab validation remain pending.';
const maintenance=[
['U01','Windows cumulative / .NET','Normal supported online servicing','Reviewed Catalog packages and required checkpoints','Authenticity, applicability, order, installed KB/build and reboot','Workflow prepared','',U.checkpoint,resumed],
['U02','Defender intelligence / engine / platform','Keep normal cloud and update services','Supported offline package distribution for all three components','Versions and freshness; cloud capability explicitly unavailable','Workflow prepared','',U.av,resumed],
['U03','Trusted / disallowed certificates','Retain trust-list and revocation maintenance','Plan authenticated local trust-list refresh','Current provenance and revocation limitations; validation remains enabled','Research','',U.trust,resumed],
['U04','Tor / Zen / Helium / apps','Keep publisher security update channels','Transfer authenticated installers and dependencies','Versions, signatures and browser launch/security behavior','Research','',S.design,resumed],
['U05','Firmware / drivers / Secure Boot / WinRE','Retain vendor and OS maintenance','Separate vendor-supported offline methods','Per-component coverage; OS build alone is insufficient','Research','',S.design,resumed],
['U06','Offline scan metadata','Online scan remains available','Fresh signed Wsusscn2.cab with offline WUA search','Catalog date, limited scope, temporary registration cleanup','Scanner planned','',U.scan,resumed],
['U07','Bundle integrity','Local verifier available','Same verifier; no network calls or installers invoked','19 synthetic tests; hashes do not establish publisher authenticity','Implemented','',U.local,resumed],
['U08','Isolation boundary','Online traffic intentionally permitted','Disconnected protected Windows; separate connected collector','External verification; online cloud protection unavailable','Not deployed','',S.design,resumed]
];
// Preserve the user's editable progress fields when regenerating this owned workbook.
const existingPath=path.join(out,'Windows-privacy-register.xlsx');
try{
const prior=await SpreadsheetFile.importXlsx(await FileBlob.load(existingPath));
const oldControls=prior.worksheets.getItem('Controls').getUsedRange().values;
const progress=new Map(oldControls.slice(5).filter(r=>r[0]).map(r=>[r[0],r]));
for(const r of controls){const old=progress.get(r[0]);if(old){r[5]=old[5];r[6]=old[6];}}
const oldRoadmap=prior.worksheets.getItem('Roadmap').getUsedRange().values;
const tasks=new Map(oldRoadmap.slice(5).filter(r=>r[0]).map(r=>[r[0],r]));
for(const r of roadmap){const old=tasks.get(r[0]);if(old)for(let i=6;i<=9;i++)r[i]=old[i]??null;}
}catch(e){if(e.code!=='ENOENT')throw e;}
const wb=Workbook.create();
const colors={navy:'#17324D',teal:'#136F73',ink:'#25364A',muted:'#546678',pale:'#EDF3F7',line:'#D8E2EA',input:'#EEF5FF',amber:'#FFF0CE'};
const sheets={};
function col(n){let t='';for(n++;n;n=Math.floor((n-1)/26))t=String.fromCharCode(65+(n-1)%26)+t;return t;}
function base(name,title,subtitle,headers,rows,widths){
const sh=wb.worksheets.add(name);sheets[name]=sh;sh.showGridLines=false;sh.tabColor=colors.teal;
const end=col(headers.length-1),last=rows.length+5;
sh.getRange(`A1:${end}${last}`).format={font:{name:'Arial',size:11,color:colors.ink},verticalAlignment:'top',wrapText:true};
const titleEnd=col(Math.min(4,headers.length-1));
sh.getRange('A2').values=[[title]];sh.getRange(`A2:${titleEnd}2`).merge();sh.getRange(`A2:${titleEnd}2`).format={fill:colors.navy,font:{name:'Arial',size:16,bold:true,color:'#FFFFFF'},rowHeight:32};
sh.getRange('A3').values=[[subtitle]];sh.getRange(`A3:${titleEnd}3`).merge();sh.getRange(`A3:${titleEnd}3`).format={font:{name:'Arial',size:11,color:colors.muted},rowHeight:46,wrapText:true};
sh.getRange(`A5:${end}5`).values=[headers];
sh.getRange(`A6:${end}${last}`).values=rows;
const gap=headers.indexOf(' ');
const tableEnd=gap>=0?col(gap-1):end;
const table=sh.tables.add(`A5:${tableEnd}${last}`,true,name.replaceAll(' ','')+'Table');table.showFilterButton=true;
sh.getRange(`A5:${end}5`).format={fill:colors.navy,font:{name:'Arial',size:11,bold:true,color:'#FFFFFF'},rowHeight:32,wrapText:true};
for(let i=0;i<widths.length;i++)sh.getRange(`${col(i)}1:${col(i)}${last}`).format.columnWidth=widths[i];
for(let i=0;i<rows.length;i++){
const r=i+6;
if(i%2===1)sh.getRange(`A${r}:${end}${r}`).format.fill=colors.pale;
let lines=1;rows[i].forEach((v,j)=>{if(typeof v==='string')lines=Math.max(lines,v.split('\n').reduce((n,x)=>n+Math.ceil(x.length/Math.max(5,widths[j]-3)),0));});
sh.getRange(`A${r}:${end}${r}`).format.rowHeight=Math.min(165,Math.max(50,lines*15+12));
}
sh.freezePanes.freezeRows(5);sh.freezePanes.freezeColumns(2);
if(gap>=0){sh.getRange(`${col(gap)}5:${col(gap)}${last}`).values=[['']];sh.getRange(`${col(gap)}5:${col(gap)}${last}`).format.fill='#FFFFFF';}
return {sh,last};
}
const overview=wb.worksheets.add('Overview');sheets.Overview=overview;
const cr=base('Controls','Windows privacy control register','Implementation describes the software. Applied and verification describe this project\'s changes to the host. Blue status cells can be updated after testing.',
['ID','Control','Area','Choice','Implementation','Applied on host','Effect verified','Intended behavior','Limits / mechanism','Compatibility','Verification / rollback',' ','Reference','Reviewed'],controls,[23,32,21,13,17,18,17,54,58,44,54,3,66,14]);
cr.sh.getRange(`F6:G${cr.last}`).format.fill=colors.input;
cr.sh.getRange(`F6:F${cr.last}`).dataValidation={rule:{type:'list',values:['Not applied','Applied','Restored']}};
cr.sh.getRange(`G6:G${cr.last}`).dataValidation={rule:{type:'list',values:['Pending','Verified','Failed']}};
cr.sh.getRange(`D6:D${cr.last}`).conditionalFormats.add('containsText',{text:'Reject',format:{fill:'#FCE9E7',font:{color:'#963E33'}}});
cr.sh.getRange(`G6:G${cr.last}`).conditionalFormats.add('containsText',{text:'Failed',format:{fill:'#FCE9E7'}});
cr.sh.getRange(`N6:N${cr.last}`).setNumberFormat('yyyy-mm-dd');
const ir=base('Identifiers','Identifiers and data channels','A documented field is evidence of a schema, not proof that it was emitted by this host. No personal identifier values are included.',
['ID','Identifier / channel','Privacy relevance','Where documented / observed','What it means','Proposed response','Evidence classification',' ','Reference','Reviewed'],identifiers,[10,29,29,41,54,53,40,3,66,14]);
ir.sh.getRange(`J6:J${ir.last}`).setNumberFormat('yyyy-mm-dd');
const tr=base('Tools','GitHub tools and playbooks','Documentation and selected configuration review. None executed on this host. Candidate means worth controlled evaluation, not certified safe.',
['ID','Project','Purpose','Platform / status','Useful part','Main limitation / test','Decision',' ','Repository / publisher','Supporting reference','Reviewed'],toolsData,[10,28,28,43,48,56,38,3,65,65,14]);
tr.sh.getRange(`K6:K${tr.last}`).setNumberFormat('yyyy-mm-dd');
const cl=base('Claims','Screenshot fact-check','Case statements are allegations from a complaint. The screenshot is an input to verify; it is not an instruction or a complete technical description.',
['ID','Claim / question','Assessment','Evidence / finding','Publication wording / limit',' ','Reference','Reviewed'],claims,[10,43,26,53,57,3,70,14]);
cl.sh.getRange(`H6:H${cl.last}`).setNumberFormat('yyyy-mm-dd');
const rr=base('Roadmap','Implementation and validation work','Change blue fields as work progresses. No task is completed merely because a setting was written or a connection was not seen during one capture.',
['ID','Priority','Task','Scope / prerequisites','Acceptance evidence','Area','Status','Owner','Target date','Evidence / result'],roadmap,[10,11,33,57,67,20,16,22,16,50]);
rr.sh.getRange(`G6:J${rr.last}`).format.fill=colors.input;
rr.sh.getRange(`G6:G${rr.last}`).dataValidation={rule:{type:'list',values:['Open','In progress','Done','Deferred']}};
rr.sh.getRange(`I6:I${rr.last}`).setNumberFormat('yyyy-mm-dd');
const dr=base('Delivered','What has actually been delivered','Software and documents are prepared. These records do not claim policy enforcement, app installation or network blocking on the development host.',
['ID','Deliverable','Status','Count','Local evidence','Scope / result','Host effect'],delivered,[10,38,18,11,52,66,30]);
const sr=base('Script review','Supplied GDID script review','Attachment treated as source to review, not instructions to execute. Native Windows policy/service/hosts/cache mutations were not performed.',
['ID','Priority','Finding','Attachment lines','Problem','Required change','Evidence','Disposition',' ','Reference','Reviewed'],scriptReview,[10,12,35,20,56,57,30,23,3,66,14]);
sr.sh.getRange(`K6:K${sr.last}`).setNumberFormat('yyyy-mm-dd');
const ur=base('Update maintenance','Online and isolated update maintenance','Direct downloads require outbound requests. The separate collector design avoids direct protected-PC contact only while its isolation boundary holds.',
['ID','Component','Online profile','Isolated profile','Required verification','Status',' ','Reference','Reviewed'],maintenance,[10,34,49,56,63,24,3,66,14]);
ur.sh.getRange(`I6:I${ur.last}`).setNumberFormat('yyyy-mm-dd');
overview.showGridLines=false;overview.tabColor=colors.navy;
overview.getRange('A1:H36').format={font:{name:'Arial',size:11,color:colors.ink},wrapText:true,verticalAlignment:'top'};
overview.getRange('A1:A36').format.columnWidth=3;
overview.getRange('B1:B36').format.columnWidth=42;overview.getRange('C1:C36').format.columnWidth=15;overview.getRange('D1:D36').format.columnWidth=3;
overview.getRange('E1:H36').format.columnWidth=23;
overview.getRange('B2:H2').merge();overview.getRange('B2').values=[['Windows privacy research and delivery register']];
overview.getRange('B2:H2').format={fill:colors.navy,font:{name:'Arial',size:16,bold:true,color:'#FFFFFF'},rowHeight:36};
overview.getRange('B3:H3').merge();overview.getRange('B3').values=[['Reviewed 18–19 September 2026. Preserve security while reducing unnecessary collection and retention.']];overview.getRange('B3:H3').format.rowHeight=30;
overview.getRange('B5:C5').values=[['Measure','Count']];overview.getRange('B5:C5').format={fill:colors.navy,font:{bold:true,color:'#FFFFFF'},rowHeight:26};
overview.getRange('B6:B12').values=[['Controls and design decisions'],['Implemented catalog / toolkit items'],['Prepared application choices'],['Applied to this host by the project'],['Applied and effect verified'],['Tools / playbooks reviewed'],['Open roadmap tasks']];
overview.getRange('C6:C12').formulas=[
[`=COUNTA(Controls!A6:A${cr.last})`],
[`=COUNTIF(Controls!E6:E${cr.last},"Implemented")`],
[`=COUNTIF(Controls!E6:E${cr.last},"Prepared")`],
[`=COUNTIF(Controls!F6:F${cr.last},"Applied")`],
[`=COUNTIFS(Controls!F6:F${cr.last},"Applied",Controls!G6:G${cr.last},"Verified")`],
[`=COUNTA(Tools!A6:A${tr.last})`],
[`=COUNTIF(Roadmap!G6:G${rr.last},"Open")`]
];
overview.getRange('B6:C12').format.rowHeight=29;overview.getRange('C6:C12').setNumberFormat('0');overview.getRange('C6:C12').format.font={size:15,bold:true,color:colors.teal};
const notes=[
['E5:H5','Current host'],['E6:H8','Windows 11 Home (Core), build 26200. The prototype declines automatic policy enforcement here. No policy changes, firewall blocks or app installs were performed by this project.'],
['E10:H10','Security boundary'],['E11:H13','Online mode retains cloud protection. Isolated mode uses transferred updates and loses online cloud/reputation capabilities. Both preserve local protections. The verifier does not establish isolation or complete patching.'],
['B15:H15','Recommended direction'],['B16:H18','Use the small reviewed Wind catalog as the base. Borrow selected ideas from maintained tools only after source review and VM tests. Evaluate one network filter separately. Avoid chaining debloat presets or claiming a universal GDID reset.'],
['B20:H20','How to use this workbook'],['B21:H23','Controls separates implementation, application and verification. Identifiers, Tools and Claims track evidence. Roadmap and Delivered track progress. Script review assesses the attachment. Update maintenance compares online and isolated operation. Scroll right for sources.'],
['B25:H25','What the labels mean'],['B26:H28','Default = proposed standard behavior; Strict/Optional = explicit choice; Keep = preserve a protection; Reject = excluded approach. Implemented = code exists, not deployment approval. Prepared = installer or setup path exists. Planned/Research = further work required.'],
['B30:H30','Evidence and maintenance'],['B31:H34','Sources are adjacent to their claims and dated. Publisher documentation is not an independent audit. Network payloads and backend correlation remain partly unknown. Editable blue status fields require evidence before marking Applied or Verified. Recheck Windows and application versions before publishing or deploying.']
];
for(const [r,t]of notes){overview.getRange(r).merge();overview.getRange(r.split(':')[0]).values=[[t]];if(/^\w+\d+:\w+\d+$/.test(r)&&r.match(/\d+/g)[0]===r.match(/\d+/g)[1])overview.getRange(r).format={fill:colors.navy,font:{bold:true,color:'#FFFFFF'},rowHeight:24};else overview.getRange(r).format.fill=colors.pale;}
overview.getRange('B31:H34').format.fill='#FFF4DB';overview.freezePanes.freezeRows(3);
// Formula sensitivity check. The test edit is restored before export.
const savedStatus=cr.sh.getRange('F6:G6').values;
const baseApplied=controls.filter(r=>r[5]==='Applied').length;
const baseVerified=controls.filter(r=>r[5]==='Applied'&&r[6]==='Verified').length;
cr.sh.getRange('F6:G6').values=[['Applied','Verified']];
if(overview.getRange('C9').values[0][0]!==baseApplied+(savedStatus[0][0]==='Applied'?0:1)||overview.getRange('C10').values[0][0]!==baseVerified+(savedStatus[0][0]==='Applied'&&savedStatus[0][1]==='Verified'?0:1))throw new Error('Summary formulas did not react to status edits.');
cr.sh.getRange('F6:G6').values=savedStatus;
wb.recalculate();
if(overview.getRange('C9').values[0][0]!==baseApplied||overview.getRange('C10').values[0][0]!==baseVerified)throw new Error('Test status was not restored.');
const checks=await wb.inspect({kind:'match',searchTerm:'#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A|#NUM!|#NULL!|#SPILL!|#CALC!',options:{useRegex:true,maxResults:100},maxChars:5000});
console.log('Formula error scan:',checks.ndjson);
console.log((await wb.inspect({kind:'region',sheetId:'Overview',range:'B5:C12',maxChars:4000,tableMaxRows:10,tableMaxCols:2})).ndjson);
await (await SpreadsheetFile.exportXlsx(wb)).save(path.join(out,'Windows-privacy-register.xlsx'));
for(const [name,range,suffix]of [['Overview','A1:H35',''],['Controls','A82:K86','offline'],['Identifiers','A24:G26','offline'],['Tools','A22:G24','offline'],['Claims','A23:E25','offline'],['Roadmap','A26:G29','offline'],['Delivered','A14:G15','offline'],['Update maintenance','A2:F13','']]){
const blob=await wb.render({sheetName:name,range,scale:1,format:'png'});await fs.writeFile(path.join(out,`preview-${name}${suffix?'-'+suffix:''}.png`),new Uint8Array(await blob.arrayBuffer()));
}
await fs.writeFile(path.join(out,'register-data.json'),JSON.stringify({reviewed:'2026-09-18/19',counts:{controls:controls.length,tools:toolsData.length,identifiers:identifiers.length,claims:claims.length,todos:roadmap.length,scriptFindings:scriptReview.length},controls,identifiers,tools:toolsData,claims,roadmap,delivered,scriptReview,maintenance},null,2));
console.log(JSON.stringify({output:path.join(out,'Windows-privacy-register.xlsx'),controls:controls.length,tools:toolsData.length,identifiers:identifiers.length,claims:claims.length,todos:roadmap.length}));
+135
View File
@@ -0,0 +1,135 @@
#Requires -Version 5.1
[CmdletBinding()]param()
Set-StrictMode -Version Latest;$ErrorActionPreference='Stop'
if([Environment]::OSVersion.Platform-ne[PlatformID]::Win32NT){throw 'Windows is required.'}
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')
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 Invoke-WindChild {param([string]$Script,[hashtable]$Parameters=@{});& (Join-Path $PSScriptRoot $Script) @Parameters|Out-Host}
$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)
$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))
Add-WindLabel $privacy 'The starting preset is selected from Windows and profile age. Every apply operation still requires exact-plan review.' 20 18 700 28|Out-Null
$preset=[Windows.Forms.ComboBox]::new();$preset.DropDownStyle='DropDownList';$preset.Location=[Drawing.Point]::new(530,52);$preset.Width=230;$privacy.Controls.Add($preset);Add-WindLabel $privacy 'Automatic preset' 530 20 220 28|Out-Null
$presetCatalog=Get-MsPrivatePresetCatalog;$installContext=Get-MsPrivateInstallContext;[void]$preset.Items.Add('Custom');foreach($item in $presetCatalog.Presets){[void]$preset.Items.Add($item.Label)}
$profile=[Windows.Forms.ComboBox]::new();$profile.DropDownStyle='DropDownList';$profile.Items.AddRange(@('Standard','Strict'));$profile.SelectedIndex=1;$profile.Location=[Drawing.Point]::new(24,58);$profile.Width=220;$privacy.Controls.Add($profile)
$updates=[Windows.Forms.ComboBox]::new();$updates.DropDownStyle='DropDownList';$updates.Items.AddRange(@('Keep','Automatic','Notify','Manual'));$updates.SelectedIndex=0;$updates.Location=[Drawing.Point]::new(270,58);$updates.Width=220;$privacy.Controls.Add($updates)
$services=Add-WindCheckBox $privacy 'Disable connected-device, cloud-sync, diagnostic, location and WER services' 24 110 $true 720
$accounts=Add-WindCheckBox $privacy 'Sign out Microsoft app accounts (Store, Xbox, packaged Office/Teams, shared account broker)' 24 148 $true 760
$profile.Add_SelectedIndexChanged({$strict=([string]$profile.SelectedItem-eq'Strict');$services.Checked=$strict;$accounts.Checked=$strict})
$recall=Add-WindCheckBox $privacy 'Disable Recall policy and delete existing Recall snapshots' 24 188 $false 620
$removeRecall=Add-WindCheckBox $privacy 'Also disable the Recall optional feature (may require reboot)' 48 222 $false 620
Add-WindLabel $privacy 'Strict blocks future Microsoft app authentication. Account purge permanently deletes current-user app sign-in data and matching stored credentials; it does not remove the Windows logon identity.' 24 258 730 72|Out-Null
$installContextLabel=Add-WindLabel $privacy ("Detected: $($installContext.Classification). Recommended: $($installContext.RecommendedPreset). $($installContext.Reason)") 24 338 730 68;$installContextLabel.ForeColor=[Drawing.Color]::FromArgb(75,85,95)
Add-WindLabel $apps 'Selected browsers are installed and verified before the first app removal.' 20 18 700 28|Out-Null
$zen=Add-WindCheckBox $apps 'Zen Browser' 24 58 $true 250;$helium=Add-WindCheckBox $apps 'Helium' 24 90 $false 250;$tor=Add-WindCheckBox $apps 'Tor Browser' 24 122 $false 250
$bundled=Add-WindCheckBox $apps 'Remove curated bundled/promotional apps' 330 58 $true 420;$oneDrive=Add-WindCheckBox $apps 'Remove OneDrive' 330 90 $true 420;$edge=Add-WindCheckBox $apps 'Remove Edge after browser verification' 330 122 $true 420
$mullvad=Add-WindCheckBox $apps 'Install Mullvad VPN (configuration remains manual)' 24 172 $false 350;$veracrypt=Add-WindCheckBox $apps 'Install VeraCrypt' 400 172 $false 300
$personal=Add-WindCheckBox $apps 'Remove every removable personal / third-party app' 24 216 $false 520
$store=Add-WindCheckBox $apps 'Remove Microsoft Store and Store Purchase App for this user' 24 250 $false 620
Add-WindLabel $apps 'Fresh Install, Medium and stronger presets select both options. Established PC keeps every application. WebView2, security tools, drivers, runtimes, servicing, chosen browsers and recovery components remain protected.' 24 290 710 70|Out-Null
Add-WindLabel $network 'Observe changes nothing. Optional telemetry blocks only the reviewed endpoint catalog. Isolation blocks default outbound traffic and automatically rolls back.' 20 18 730 58|Out-Null
$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
$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}})
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)
$sensorItems=@();try{$sensorItems=@(Get-WindSensorInventory);foreach($d in $sensorItems){[void]$deviceList.Items.Add("$($d.Kind): $($d.Name) [$($d.Status)]")}}catch{Add-WindLabel $devices ('Inventory unavailable: '+$_.Exception.Message) 24 410 700 50|Out-Null}
$cleanup=Add-WindCheckBox $maintenance 'Run safe temporary-file cleanup now (files older than 7 days)' 24 40 $false 650
$privacyCleanup=Add-WindCheckBox $maintenance 'Clear privacy artifacts now: WER/crash dumps, recent items, thumbnails, browser caches, PowerShell history and clipboard' 24 76 $false 760
$cleanupSchedule=Add-WindCheckBox $maintenance 'Install daily Temp and expired ms-private-report cleanup' 24 112 $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
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}})
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)}
function Get-WindGuiAppPlan {
$inventory=@(Get-WindAppInventory)
@(Get-WindAppPlan -Inventory $inventory -SkipBundled:(-not$bundled.Checked) -KeepOneDrive:(-not$oneDrive.Checked) -KeepEdge:(-not$edge.Checked) -RemovePersonal:$personal.Checked -RemoveStore:$store.Checked)
}
function Set-WindGuiPreset {
param($p)
$script:applyingPreset=$true
try{
$profile.SelectedItem=$p.Profile;$updates.SelectedItem=$p.Updates;$services.Checked=$p.Services;$accounts.Checked=$p.Accounts
$zen.Checked=$p.Browsers-contains'Zen';$helium.Checked=$p.Browsers-contains'Helium';$tor.Checked=$p.Browsers-contains'Tor'
$bundled.Checked=$p.RemoveBundled;$personal.Checked=$p.RemovePersonal;$store.Checked=$p.RemoveStore;$oneDrive.Checked=$p.RemoveOneDrive;$edge.Checked=$p.RemoveEdge
$mullvad.Checked=$p.InstallMullvad;$veracrypt.Checked=$p.InstallVeraCrypt;$recall.Checked=$p.Recall;$removeRecall.Checked=$p.RemoveRecall
$netProfile.SelectedItem=$p.Network;$rollback.Value=[decimal]$p.RollbackMinutes;$cleanup.Checked=$p.Cleanup;$privacyCleanup.Checked=$p.PrivacyCleanup;$cleanupSchedule.Checked=$p.CleanupSchedule;$lock.Checked=$p.Lock
for($i=0;$i-lt$deviceList.Items.Count;$i++){$deviceList.SetItemChecked($i,[bool]$p.DisableSensors)}
}finally{$script:applyingPreset=$false}
}
function Get-WindGuiReviewText {
param([object[]]$AppPlan,[object[]]$PolicyPlan=@())
$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)",'')
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($PolicyPlan.Count){$lines+=''}
foreach($a in $remove){$lines+="REMOVE [$($a.Scope)] $($a.Name) $($a.Version) - $($a.Reason)"}
if($deviceList.CheckedIndices.Count){$lines+='';foreach($i in $deviceList.CheckedIndices){$lines+="DISABLE $($deviceList.Items[[int]$i])"}}
$lines -join [Environment]::NewLine
}
function Show-WindGuiReview {
param([string]$Text)
$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
$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)
}
$script:applyingPreset=$false
$markCustom={if(-not$script:applyingPreset-and[string]$preset.SelectedItem-ne'Custom'){$preset.SelectedItem='Custom'}}
foreach($control in @($profile,$updates,$netProfile)){$control.Add_SelectedValueChanged($markCustom)}
foreach($control in @($services,$accounts,$recall,$removeRecall,$zen,$helium,$tor,$bundled,$oneDrive,$edge,$personal,$store,$mullvad,$veracrypt,$cleanup,$privacyCleanup,$cleanupSchedule,$lock)){$control.Add_CheckStateChanged($markCustom)}
$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.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)
$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)
$review.Add_Click({
try{$appPlan=Get-WindGuiAppPlan;$policyPlan=@(Get-WindPlan -Context (Get-WindContext) -Store (New-WindRegistryStore) -Profile ([string]$profile.SelectedItem) -Updates ([string]$updates.SelectedItem));Show-WindGuiReview (Get-WindGuiReviewText $appPlan $policyPlan)}catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message,'ms-private error')|Out-Null}
})
$apply.Add_Click({
try{
$id=[Security.Principal.WindowsIdentity]::GetCurrent();$admin=[Security.Principal.WindowsPrincipal]::new($id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator);$id.Dispose();if(-not$admin){throw 'Apply requires an elevated PowerShell started with the same Windows account.'}
$appPlan=Get-WindGuiAppPlan;$appIssues=@($appPlan|Where-Object {$_.Status -eq 'Blocked'})
$policyPlan=@(Get-WindPlan -Context (Get-WindContext) -Store (New-WindRegistryStore) -Profile ([string]$profile.SelectedItem) -Updates ([string]$updates.SelectedItem));$policyIssues=@($policyPlan|Where-Object {$_.Status -in @('Conflict','ReadError')})
$recallPlan=if($recall.Checked){Get-WindRecallPlan -RemoveFeature:$removeRecall.Checked}else{$null};$recallDisposition=Resolve-MsPrivateRecallDisposition -Requested $recall.Checked -Plan $recallPlan
$policyIds=@($policyPlan|Where-Object {$_.Status -in @('WouldChange','Configured')}|ForEach-Object Id)
Show-WindGuiReview (Get-WindGuiReviewText $appPlan $policyPlan)
$removeCount=@($appPlan|Where-Object {$_.Status -eq 'Remove'}).Count
$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)}
$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($services.Checked){$groups.Add([pscustomobject]@{Id='Services';Label='Service configuration';DependsOn=@();Action=({Invoke-WindChild '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($tools.Count){$groups.Add([pscustomobject]@{Id='PrivacyTools';Label='Privacy-tool installation';DependsOn=@();Action=({Invoke-WindChild '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($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($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($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($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($cleanupSchedule.Checked){$groups.Add([pscustomobject]@{Id='CleanupSchedule';Label='Cleanup schedule';DependsOn=@();Action=({Invoke-WindChild '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())})}
$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
$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}}}
$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
}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}
})
[void]$form.ShowDialog()
+10 -9
View File
@@ -2,7 +2,7 @@
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
param( param(
[ValidateSet('Menu','Audit','Plan','Apply','Restore','Guide')][string]$Mode = 'Menu', [ValidateSet('Menu','Audit','Plan','Apply','Restore','Guide')][string]$Mode = 'Menu',
[ValidateSet('Standard','Strict')][string]$Profile = 'Standard', [ValidateSet('Standard','Strict')][string]$Profile = 'Strict',
[ValidateSet('Keep','Automatic','Notify','Manual')][string]$Updates = 'Keep', [ValidateSet('Keep','Automatic','Notify','Manual')][string]$Updates = 'Keep',
[string[]]$Id = @(), [string[]]$Id = @(),
[string]$Journal, [string]$Journal,
@@ -10,18 +10,18 @@ param(
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1') -Force Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1')
if ($Mode -eq 'Menu') { if ($Mode -eq 'Menu') {
if ($Json -or $Id.Count -or $Journal) { throw 'Use an explicit Audit, Plan, Apply or Restore mode with these parameters.' } if ($Json -or $Id.Count -or $Journal) { throw 'Use an explicit Audit, Plan, Apply or Restore mode with these parameters.' }
if ($WhatIfPreference) { Write-Output 'Preview: launch Wind menu; no settings or applications changed.'; return } if ($WhatIfPreference) { Write-Output 'Preview: launch ms-private menu; no settings or applications changed.'; return }
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Tui.psm1') -Force Import-Module (Join-Path $PSScriptRoot 'src/Wind.Tui.psm1')
Start-WindMenu -Root $PSScriptRoot -Profile $Profile -Updates $Updates Start-WindMenu -Root $PSScriptRoot -Profile $Profile -Updates $Updates
return return
} }
if ($Mode -eq 'Guide') { if ($Mode -eq 'Guide') {
Get-Content -LiteralPath (Join-Path $PSScriptRoot 'docs/HOME-GUIDE.md') Get-Content -LiteralPath (Join-Path $PSScriptRoot 'docs/HOME-GUIDE.md') -Encoding UTF8
return return
} }
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' }
@@ -39,7 +39,7 @@ if ($Mode -in @('Audit','Plan')) {
return return
} }
$stateRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'WindPrivacy' $stateRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate'
$markerPath = Join-Path $stateRoot 'installation-id.txt' $markerPath = Join-Path $stateRoot 'installation-id.txt'
if ($Mode -eq 'Restore') { if ($Mode -eq 'Restore') {
if ([string]::IsNullOrWhiteSpace($Journal)) { throw 'Restore requires -Journal.' } if ([string]::IsNullOrWhiteSpace($Journal)) { throw 'Restore requires -Journal.' }
@@ -59,11 +59,11 @@ if ($Mode -eq 'Restore') {
if (-not $PSCmdlet.ShouldProcess('Local Windows machine policies', "$Mode selected privacy settings")) { return } if (-not $PSCmdlet.ShouldProcess('Local Windows machine policies', "$Mode selected privacy settings")) { return }
Assert-WindMutationAllowed Assert-WindMutationAllowed
$mutex = [Threading.Mutex]::new($false, 'Global\WindPrivacyPolicyTransaction') $mutex = [Threading.Mutex]::new($false, 'Global\MsPrivatePolicyTransaction')
$locked = $false $locked = $false
try { try {
try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true } try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true }
if (-not $locked) { throw 'Another Wind transaction is running.' } if (-not $locked) { throw 'Another ms-private transaction is running.' }
if ($Mode -eq 'Apply') { if ($Mode -eq 'Apply') {
# Refresh under the lock; never apply stale state captured before the operation. # Refresh under the lock; never apply stale state captured before the operation.
$plan = @(Get-WindPlan -Context $context -Store $store -Profile $Profile -Updates $Updates -Id $Id) $plan = @(Get-WindPlan -Context $context -Store $store -Profile $Profile -Updates $Updates -Id $Id)
@@ -78,7 +78,8 @@ try {
$saved = Get-Content -LiteralPath $Journal -Raw | ConvertFrom-Json $saved = Get-Content -LiteralPath $Journal -Raw | ConvertFrom-Json
Assert-WindJournal $saved $installationId Assert-WindJournal $saved $installationId
} }
$save = { param($data) Save-WindJournal -Journal $data -Path $Journal }.GetNewClosure() $journalWriter = Get-Command Save-WindJournal -CommandType Function
$save = { param($data) & $journalWriter -Journal $data -Path $Journal }.GetNewClosure()
Write-Output "Recovery journal: $Journal" Write-Output "Recovery journal: $Journal"
if ($Mode -eq 'Apply') { $result = Invoke-WindTransaction -Plan $plan -Context $context -Store $store -Save $save -InstallationId $installationId } if ($Mode -eq 'Apply') { $result = Invoke-WindTransaction -Plan $plan -Context $context -Store $store -Save $save -InstallationId $installationId }
else { $result = Restore-WindTransaction -Journal $saved -Store $store -Save $save -InstallationId $installationId } else { $result = Restore-WindTransaction -Journal $saved -Store $store -Save $save -InstallationId $installationId }
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<DefaultItemExcludes>$(DefaultItemExcludes);bin/**;obj/**</DefaultItemExcludes>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\MsPrivate.Launcher\PayloadInstaller.cs" Link="PayloadInstaller.cs" />
</ItemGroup>
</Project>
@@ -0,0 +1,58 @@
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
using MsPrivate.Launcher;
var passed = 0;
void Assert(bool condition, string message) { if (!condition) throw new Exception(message); passed++; }
byte[] CreateArchive(string path, byte[] content, string? declaredPath = null, string? declaredHash = null, bool extra = false)
{
var manifest = new PayloadManifest
{
Schema = 1,
Version = "test",
EntryPoint = declaredPath ?? path,
Files = [new PayloadFile { Path = declaredPath ?? path, Length = content.Length, Sha256 = declaredHash ?? Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant() }]
};
using var memory = new MemoryStream();
using (var archive = new ZipArchive(memory, ZipArchiveMode.Create, leaveOpen: true))
{
var file = archive.CreateEntry(path);
using (var output = file.Open()) output.Write(content);
var metadata = archive.CreateEntry(PayloadInstaller.ManifestName);
using (var output = metadata.Open()) JsonSerializer.Serialize(output, manifest);
if (extra) archive.CreateEntry("undeclared.txt");
}
return memory.ToArray();
}
var root = Path.Combine(Path.GetTempPath(), "ms-private-launcher-tests-" + Guid.NewGuid().ToString("N"));
try
{
var content = "Write-Output test"u8.ToArray();
var valid = CreateArchive("ms-private-GUI.ps1", content);
var manifest = PayloadInstaller.ReadAndValidateArchive(valid);
Assert(manifest.Files.Count == 1, "Valid embedded archive was rejected.");
var install = Path.Combine(root, "runtime");
PayloadInstaller.EnsureInstalled(valid, install);
Assert(PayloadInstaller.IsValidInstallation(install, manifest), "Installed payload did not verify.");
File.WriteAllText(Path.Combine(install, "ms-private-GUI.ps1"), "tampered");
PayloadInstaller.EnsureInstalled(valid, install);
Assert(PayloadInstaller.IsValidInstallation(install, manifest), "Tampered installed payload was not repaired.");
File.WriteAllText(Path.Combine(install, "unexpected.ps1"), "not declared");
Assert(!PayloadInstaller.IsValidInstallation(install, manifest), "Unexpected installed file was accepted.");
PayloadInstaller.EnsureInstalled(valid, install);
Assert(PayloadInstaller.IsValidInstallation(install, manifest), "Installation with an unexpected file was not repaired.");
File.WriteAllText(Path.Combine(install, PayloadInstaller.ManifestName), "{}");
Assert(!PayloadInstaller.IsValidInstallation(install, manifest), "Modified installed manifest was accepted.");
PayloadInstaller.EnsureInstalled(valid, install);
Assert(PayloadInstaller.IsValidInstallation(install, manifest), "Modified installed manifest was not repaired.");
try { PayloadInstaller.ReadAndValidateArchive(CreateArchive("safe.ps1", content, "../escape.ps1")); throw new Exception("Traversal manifest was accepted."); } catch (InvalidDataException) { passed++; }
try { PayloadInstaller.ReadAndValidateArchive(CreateArchive("ms-private-GUI.ps1", content, declaredHash: new string('0', 64))); throw new Exception("Bad hash was accepted."); } catch (InvalidDataException) { passed++; }
try { PayloadInstaller.ReadAndValidateArchive(CreateArchive("ms-private-GUI.ps1", content, extra: true)); throw new Exception("Undeclared archive entry was accepted."); } catch (InvalidDataException) { passed++; }
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
Console.WriteLine($"{passed} native launcher tests passed.");
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<DefaultItemExcludes>$(DefaultItemExcludes);bin/**;obj/**</DefaultItemExcludes>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>ms-private</AssemblyName>
<RootNamespace>MsPrivate.Launcher</RootNamespace>
<Version>0.0.1</Version>
<AssemblyVersion>0.0.1.0</AssemblyVersion>
<FileVersion>0.0.1.0</FileVersion>
<Product>ms-private</Product>
<Description>Windows privacy control center</Description>
</PropertyGroup>
<ItemGroup Condition="'$(PayloadZip)' != ''">
<EmbeddedResource Include="$(PayloadZip)" LogicalName="MsPrivate.Payload.zip" />
</ItemGroup>
</Project>
@@ -0,0 +1,180 @@
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
namespace MsPrivate.Launcher;
internal sealed class PayloadManifest
{
public int Schema { get; set; }
public string Version { get; set; } = "";
public string EntryPoint { get; set; } = "";
public List<PayloadFile> Files { get; set; } = [];
}
internal sealed class PayloadFile
{
public string Path { get; set; } = "";
public long Length { get; set; }
public string Sha256 { get; set; } = "";
}
internal static class PayloadInstaller
{
internal const string ManifestName = "payload.manifest.json";
internal static PayloadManifest ReadAndValidateArchive(byte[] archiveBytes)
{
using var memory = new MemoryStream(archiveBytes, writable: false);
using var archive = new ZipArchive(memory, ZipArchiveMode.Read, leaveOpen: false);
var manifestEntry = archive.GetEntry(ManifestName) ?? throw new InvalidDataException("Embedded payload manifest is missing.");
PayloadManifest manifest;
using (var stream = manifestEntry.Open())
{
manifest = JsonSerializer.Deserialize<PayloadManifest>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidDataException("Embedded payload manifest is invalid.");
}
if (manifest.Schema != 1 || string.IsNullOrWhiteSpace(manifest.Version))
throw new InvalidDataException("Unsupported payload manifest.");
var entryPoint = NormalizeRelativePath(manifest.EntryPoint);
var declared = new Dictionary<string, PayloadFile>(StringComparer.OrdinalIgnoreCase);
foreach (var file in manifest.Files)
{
var relative = NormalizeRelativePath(file.Path);
if (!declared.TryAdd(relative, file)) throw new InvalidDataException($"Duplicate payload path: {relative}");
if (file.Length < 0 || file.Sha256.Length != 64 || file.Sha256.Any(c => !Uri.IsHexDigit(c)))
throw new InvalidDataException($"Invalid payload metadata: {relative}");
}
if (!declared.ContainsKey(entryPoint)) throw new InvalidDataException("Payload entry point is not declared.");
var actualEntries = archive.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToArray();
if (actualEntries.Length != declared.Count + 1) throw new InvalidDataException("Payload archive contains undeclared entries.");
foreach (var entry in actualEntries)
{
var relative = NormalizeRelativePath(entry.FullName);
if (relative.Equals(ManifestName, StringComparison.OrdinalIgnoreCase)) continue;
if (!declared.TryGetValue(relative, out var expected)) throw new InvalidDataException($"Undeclared payload entry: {relative}");
using var stream = entry.Open();
var hash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
if (entry.Length != expected.Length || !hash.Equals(expected.Sha256, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException($"Embedded payload verification failed: {relative}");
}
return manifest;
}
internal static void EnsureInstalled(byte[] archiveBytes, string installDirectory)
{
var manifest = ReadAndValidateArchive(archiveBytes);
var destination = Path.GetFullPath(installDirectory);
if (IsValidInstallation(destination, manifest)) return;
var parent = Directory.GetParent(destination)?.FullName ?? throw new InvalidDataException("Invalid runtime destination.");
Directory.CreateDirectory(parent);
var staging = Path.Combine(parent, ".staging-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(staging);
try
{
using var memory = new MemoryStream(archiveBytes, writable: false);
using var archive = new ZipArchive(memory, ZipArchiveMode.Read, leaveOpen: false);
foreach (var file in manifest.Files)
{
var relative = NormalizeRelativePath(file.Path);
var entry = archive.GetEntry(relative) ?? throw new InvalidDataException($"Payload entry disappeared: {relative}");
var target = ResolveBelow(staging, relative);
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
using var input = entry.Open();
using var output = new FileStream(target, FileMode.CreateNew, FileAccess.Write, FileShare.None);
input.CopyTo(output);
output.Flush(flushToDisk: true);
}
File.WriteAllText(Path.Combine(staging, ManifestName), JsonSerializer.Serialize(manifest));
if (!IsValidInstallation(staging, manifest)) throw new InvalidDataException("Extracted payload verification failed.");
if (Directory.Exists(destination)) DeleteOrdinaryDirectory(destination);
Directory.Move(staging, destination);
}
finally
{
if (Directory.Exists(staging)) DeleteOrdinaryDirectory(staging);
}
}
internal static bool IsValidInstallation(string directory, PayloadManifest manifest)
{
if (!Directory.Exists(directory) || IsReparsePoint(directory)) return false;
var expectedPaths = manifest.Files
.Select(file => NormalizeRelativePath(file.Path))
.Append(ManifestName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var path in EnumerateOrdinaryFiles(directory))
{
var relative = Path.GetRelativePath(directory, path).Replace('\\', '/');
if (!expectedPaths.Remove(relative)) return false;
}
if (expectedPaths.Count != 0) return false;
var installedManifest = Path.Combine(directory, ManifestName);
if (!File.ReadAllText(installedManifest).Equals(JsonSerializer.Serialize(manifest), StringComparison.Ordinal)) return false;
foreach (var file in manifest.Files)
{
var path = ResolveBelow(directory, NormalizeRelativePath(file.Path));
if (!File.Exists(path) || (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0) return false;
var info = new FileInfo(path);
if (info.Length != file.Length) return false;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var hash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
if (!hash.Equals(file.Sha256, StringComparison.OrdinalIgnoreCase)) return false;
}
return true;
}
internal static string NormalizeRelativePath(string value)
{
if (string.IsNullOrWhiteSpace(value) || Path.IsPathRooted(value)) throw new InvalidDataException("Payload path must be relative.");
var normalized = value.Replace('\\', '/').Trim('/');
if (normalized.Length == 0 || normalized.Contains(':')) throw new InvalidDataException("Invalid payload path.");
var segments = normalized.Split('/');
if (segments.Any(s => s is "" or "." or "..")) throw new InvalidDataException("Payload path traversal was refused.");
return string.Join('/', segments);
}
internal static string ResolveBelow(string root, string relative)
{
var basePath = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
var target = Path.GetFullPath(Path.Combine(basePath, relative.Replace('/', Path.DirectorySeparatorChar)));
if (!target.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Payload escaped its runtime directory.");
return target;
}
internal static bool IsReparsePoint(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
internal static void DeleteOrdinaryDirectory(string path)
{
_ = EnumerateOrdinaryFiles(path).ToArray();
Directory.Delete(path, recursive: true);
}
private static IEnumerable<string> EnumerateOrdinaryFiles(string directory)
{
var root = new DirectoryInfo(directory);
if (!root.Exists) yield break;
if ((root.Attributes & FileAttributes.ReparsePoint) != 0)
throw new InvalidDataException("Refusing to follow a runtime reparse point.");
foreach (var entry in root.EnumerateFileSystemInfos())
{
if ((entry.Attributes & FileAttributes.ReparsePoint) != 0)
throw new InvalidDataException("Refusing to follow a runtime reparse point.");
if (entry is DirectoryInfo child)
{
foreach (var file in EnumerateOrdinaryFiles(child.FullName)) yield return file;
}
else if (entry is FileInfo file)
{
yield return file.FullName;
}
else
{
throw new InvalidDataException("Unsupported runtime entry type.");
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using System.Diagnostics;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Windows.Forms;
namespace MsPrivate.Launcher;
internal static class Program
{
private const string PayloadResource = "MsPrivate.Payload.zip";
[STAThread]
private static int Main()
{
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.EnableVisualStyles();
using var mutex = new Mutex(initiallyOwned: true, @"Local\ms-private-launcher", out var ownsMutex);
if (!ownsMutex)
{
MessageBox.Show("ms-private is already running.", "ms-private", MessageBoxButtons.OK, MessageBoxIcon.Information);
return 2;
}
try
{
if (!OperatingSystem.IsWindows() || !Environment.Is64BitOperatingSystem) throw new PlatformNotSupportedException("ms-private requires 64-bit Windows.");
using var identity = WindowsIdentity.GetCurrent();
if (!new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator)) throw new UnauthorizedAccessException("Administrator rights are required.");
var bytes = ReadPayload();
var payloadHash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
var version = Assembly.GetExecutingAssembly().GetName().Version ?? throw new InvalidDataException("The launcher version is missing.");
var productVersion = $"{version.Major}.{version.Minor}.{version.Build}";
var runtimeRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "ms-private", "runtime");
var runtimeName = productVersion + "-" + payloadHash[..12];
var runtimeDirectory = Path.Combine(runtimeRoot, runtimeName);
PayloadInstaller.EnsureInstalled(bytes, runtimeDirectory);
RemoveOlderRuntimes(runtimeRoot, runtimeDirectory);
var entryPoint = Path.Combine(runtimeDirectory, "ms-private-GUI.ps1");
var powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", "powershell.exe");
if (!File.Exists(powershell)) throw new FileNotFoundException("Windows PowerShell 5.1 was not found.", powershell);
var start = new ProcessStartInfo
{
FileName = powershell,
WorkingDirectory = runtimeDirectory,
UseShellExecute = false,
CreateNoWindow = true
};
foreach (var argument in new[] { "-NoLogo", "-NoProfile", "-ExecutionPolicy", "RemoteSigned", "-File", entryPoint }) start.ArgumentList.Add(argument);
using var process = Process.Start(start) ?? throw new InvalidOperationException("Windows PowerShell could not be started.");
process.WaitForExit();
if (process.ExitCode != 0) throw new InvalidOperationException($"The ms-private interface exited with code {process.ExitCode}.");
return 0;
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "ms-private could not start", MessageBoxButtons.OK, MessageBoxIcon.Error);
return 1;
}
finally
{
mutex.ReleaseMutex();
}
}
private static byte[] ReadPayload()
{
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(PayloadResource)
?? throw new InvalidDataException("The embedded ms-private payload is missing.");
using var memory = new MemoryStream();
stream.CopyTo(memory);
return memory.ToArray();
}
private static void RemoveOlderRuntimes(string runtimeRoot, string currentDirectory)
{
if (!Directory.Exists(runtimeRoot) || PayloadInstaller.IsReparsePoint(runtimeRoot)) return;
foreach (var directory in Directory.GetDirectories(runtimeRoot))
{
if (Path.GetFullPath(directory).Equals(Path.GetFullPath(currentDirectory), StringComparison.OrdinalIgnoreCase)) continue;
if (PayloadInstaller.IsReparsePoint(directory)) continue;
try { PayloadInstaller.DeleteOrdinaryDirectory(directory); } catch { }
}
}
}
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="0.0.1.0" name="ms-private" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>
@@ -1,73 +0,0 @@
# Why I started building a more private Windows setup
*Draft, 19 September 2026. The accompanying register separates completed development from untested plans and changes actually applied.*
The idea began with a screenshot about the arrest of an alleged Scattered Spider member. It described a Windows identifier I had barely heard of: GDID. What caught my attention was the gap between the privacy controls people recognize and the records an operating-system ecosystem can produce.
I wanted to understand that gap before downloading another “disable all telemetry” script.
## Following the screenshot back to evidence
The complaint against Peter Stokes describes Microsoft records associating an installation identifier with URLs, IP addresses and times. It is an allegation, not a conviction, and it does not identify every component responsible for collection. It also does not establish that every Windows computer reports every browser's history. [Primary complaint, paragraphs 25–29](https://www.justice.gov/usao-ndil/media/1450651/dl?inline=).
That distinction matters to me. I can be concerned about collection without turning an incomplete technical account into a universal claim.
Microsoft documents a `GlobalDeviceId` field in its Delivery Optimization reporting schema. The existence of that field does not make that table a complete web-history database. [UCDOStatus schema](https://learn.microsoft.com/en-us/windows/deployment/update/wufb-reports-schema-ucdostatus).
Independent research gave me additional leads. I reviewed [gdid-reversal](https://github.com/SmtimesIWndr/gdid-reversal) and its [follow-up](https://github.com/SmtimesIWndr/We-running-GDID-back), including their corrections. They are useful starting points for experiments, not a certification that a particular script makes Windows anonymous. The project review records pinned revisions so readers can distinguish the material examined from later edits.
## The parts I had underestimated
There is more than one identifier and more than one data channel. Microsoft's required-diagnostic schema describes PCFP, calculated from hardware identifiers, and CensusId, a hardware identifier. A published field does not prove that it was emitted by my machine, but it makes “turn off the advertising ID and you are done” an inadequate model. [Windows diagnostic schema](https://learn.microsoft.com/en-us/windows/privacy/required-diagnostic-events-fields-windows-11-24h2).
Optional diagnostics are also separate from the data used by connected features and essential services. Turning off one category does not turn off account synchronization, security services and every application's reporting. [Windows service-data explanation](https://learn.microsoft.com/en-us/windows/privacy/essential-services-and-connected-experiences).
Some of the most consequential information is not hidden behind an obscure device ID. Crash reports can contain fragments of memory. Browser accounts can synchronize activity. Installed applications have their own analytics choices. These deserve individual decisions. [Windows diagnostic configuration](https://learn.microsoft.com/en-us/windows/privacy/configure-windows-diagnostic-data-in-your-organization).
## Privacy with working security
My goal is to reduce unnecessary collection while keeping the machine defended and maintained. The standard design keeps antivirus, security updates, the firewall, disk encryption and supported boot protections.
That comes with a real tradeoff. Defender's cloud protection can send security metadata; disabling sample submission does not necessarily stop that metadata and can reduce protection. I want this explained plainly, with any optional change presented separately. [Defender cloud protection and samples](https://learn.microsoft.com/en-us/defender-endpoint/cloud-protection-microsoft-antivirus-sample-submission).
The same principle applies to blocking domains. I do not want an impressive-looking blocklist that silently stops certificate maintenance, application updates or Windows servicing. A restriction earns its place when it reduces an identified unwanted flow and passes the security workflows I intend to keep.
## What I am preparing
One question changed the architecture: could I download updates without sending anything to Microsoft? A direct download still needs outbound requests and connection metadata. An exception that lets Windows Update through the firewall also lets its update requests through.
For a protected Windows environment that must make no direct Internet connections, the plan is a separate collector: another machine downloads reviewed publisher packages, then transfers them for local installation. The protected environment stays isolated. The collector still contacts the publishers; a VPN or transparent proxy would not erase the information inside forwarded requests.
This option needs a maintenance process for more than Windows itself. Defender, browser updates, certificate trust, firmware and recovery components need attention too. Windows 11 packages may have checkpoint prerequisites. Offline operation also gives up online cloud analysis and reputation checks, even when local antivirus remains active. I want that cost visible beside the privacy benefit. [Checkpoint updates](https://learn.microsoft.com/en-us/windows/deployment/update/catalog-checkpoint-cumulative-updates), [Defender updates](https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-updates).
The first layer is a small set of documented controls: the supported diagnostic minimum, advertising-ID restrictions, Windows location and sensor permissions, online speech, cloud clipboard, cloud search and Delivery Optimization choices. Every label should describe its actual scope. Denying microphone access to covered Windows apps is different from physically disconnecting a microphone.
The second layer is optional feature removal: unused cloud integration, Widgets, Copilot-related applications, Recall retention and selected consumer/OEM apps. OneDrive needs a local-data and backup check before removal. Edge and WebView2 need separate treatment. Removing a package should not silently remove the tools needed to maintain another application.
The browser choices are Tor Browser, Zen and Helium. Tor is the option for sessions where unlinkability matters; Zen and Helium are everyday browser choices. Separate installations alone do not separate identities, especially when I sign into the same accounts. I will preserve browser updates and review sync, permissions and retention individually. [Tor guidance](https://support.torproject.org/tor-browser/security/using-tb-safely/), [Zen privacy](https://zen-browser.app/privacy-policy/), [Helium privacy](https://helium.computer/privacy).
Mullvad and VeraCrypt are optional parts of the setup. A VPN installation still needs configuration and leak testing. An encryption tool still needs a correctly created volume and a recovery plan. Neither installation deserves an “anonymous” badge.
## What cleanup can realistically do
I want less unnecessary local retention: temporary files with sensible age limits, browser-owned cleanup settings and explicit choices about history. I do not want a cleaner that quietly deletes Downloads, active application data or every troubleshooting log.
Windows locking also needs an honest description. The proposed session-lock actions are application locking, clipboard clearing and graceful unmount of selected non-system encrypted volumes. They cannot promise to erase every password, token or decrypted document from RAM. VeraCrypt's own documentation explains that other applications can retain decrypted data. [VeraCrypt memory considerations](https://veracrypt.io/en/Unencrypted%20Data%20in%20RAM.html).
That is why general registry cleaners, RAM trimming and blanket log deletion are excluded from this project.
## My own behavior belongs in the threat model
I cannot solve every privacy problem through an operating-system setting. The accounts I reuse, the things I post, the people I tell, and the identifying details inside a screenshot or document can reveal more than a technical identifier.
I do not have evidence to rank people as the single largest source of exposure in every situation. I do know that my habits are part of the system I am trying to improve. Before publishing something, I want to consider its metadata, visible account details and how it connects to things I have already shared.
## What is finished, and what is not
The project has a PowerShell prototype with audit, planning, guarded application and restoration, plus five optional application choices. Its 23 transaction/recovery tests passed in both tested PowerShell versions. Those tests concern the engine; they do not establish real-world telemetry suppression.
It now also has a local OS inventory and offline bundle-integrity verifier, with 19 additional tests passing in PowerShell 7 and 5.1. The verifier checks transferred files against a manifest and refuses unsafe paths, mismatched targets and invalid declared dependencies. It does not download or install updates, and matching hashes are not proof of publisher authenticity or complete patch coverage. Real offline installation and containment tests are still ahead.
No operating-system privacy settings or network blocks have been applied by this project to the development host. The host is Windows Home, and the prototype currently declines unsupported automatic policy enforcement there. The next evidence comes from controlled Windows testing, measured connections, security-update checks and restoration tests.
The accompanying workbook records the individual controls, excluded approaches, sources and remaining tasks. I want readers to be able to see exactly what was prepared, what was applied and what was demonstrated. That is the standard I want this project to meet before I recommend it to anyone else.
File diff suppressed because it is too large. Load diff
+84
View File
@@ -0,0 +1,84 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-MsPrivateAccountCatalog {
param([string]$Path=(Join-Path $PSScriptRoot '../config/accounts.json'))
$catalog=Get-Content -LiteralPath $Path -Raw|ConvertFrom-Json
if($catalog.Schema-ne1){throw 'Unsupported account catalog schema.'}
if(-not@($catalog.Packages).Count-or-not@($catalog.CredentialTargetPatterns).Count){throw 'Account catalog must contain packages and credential patterns.'}
$ids=@{};$names=@{};foreach($item in @($catalog.Packages)){
$id=[string]$item.Id;$name=[string]$item.Name
if($id-notmatch'^[a-z0-9][a-z0-9-]*$'-or$ids.ContainsKey($id)){throw "Invalid or duplicate account package ID: $id"}
if($name-notmatch'^[A-Za-z0-9.]+$'-or$names.ContainsKey($name)){throw "Invalid or duplicate account package: $name"}
if($item.Scope-notin@('Consumer','AllAppAccounts')){throw "Invalid account package scope: $id"}
$ids[$id]=$true;$names[$name]=$true
}
$patterns=@{};foreach($pattern in @($catalog.CredentialTargetPatterns)){
$value=[string]$pattern;if([string]::IsNullOrWhiteSpace($value)-or$patterns.ContainsKey($value)){throw 'Credential target patterns must be nonempty and unique.'}
try{[void][regex]::new($value)}catch{throw "Invalid credential target pattern: $value"};$patterns[$value]=$true
}
$catalog
}
function Initialize-MsPrivateCredentialNative {
if('MsPrivate.CredentialNative'-as[type]){return}
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace MsPrivate {
public sealed class CredentialItem { public string TargetName; public uint Type; }
public static class CredentialNative {
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct CREDENTIAL { public uint Flags; public uint Type; public IntPtr TargetName; public IntPtr Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public uint CredentialBlobSize; public IntPtr CredentialBlob; public uint Persist; public uint AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName; }
[DllImport("advapi32.dll",EntryPoint="CredEnumerateW",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool CredEnumerate(string filter,uint flags,out uint count,out IntPtr credentials);
[DllImport("advapi32.dll",EntryPoint="CredDeleteW",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool CredDelete(string target,uint type,uint flags);
[DllImport("advapi32.dll")] static extern void CredFree(IntPtr buffer);
public static CredentialItem[] Enumerate(){uint count;IntPtr array;if(!CredEnumerate(null,0,out count,out array)){int e=Marshal.GetLastWin32Error();if(e==1168)return new CredentialItem[0];throw new Win32Exception(e);}try{var result=new List<CredentialItem>();for(int i=0;i<count;i++){IntPtr p=Marshal.ReadIntPtr(array,i*IntPtr.Size);var c=(CREDENTIAL)Marshal.PtrToStructure(p,typeof(CREDENTIAL));result.Add(new CredentialItem{TargetName=Marshal.PtrToStringUni(c.TargetName),Type=c.Type});}return result.ToArray();}finally{CredFree(array);}}
public static void Delete(string target,uint type){if(!CredDelete(target,type,0))throw new Win32Exception(Marshal.GetLastWin32Error());}
}
}
'@
}
function New-MsPrivateAccountBackend {
@{
ListPackages={@(Get-AppxPackage -ErrorAction Stop|ForEach-Object{[pscustomobject]@{Name=$_.Name;PackageFullName=$_.PackageFullName}})}
ResetPackage={param($fullName)if(-not(Get-Command Reset-AppxPackage -ErrorAction SilentlyContinue)){throw 'Reset-AppxPackage is unavailable on this Windows build.'};Reset-AppxPackage -Package $fullName -Confirm:$false -ErrorAction Stop}
ListCredentials={Initialize-MsPrivateCredentialNative;@([MsPrivate.CredentialNative]::Enumerate()|ForEach-Object{[pscustomobject]@{TargetName=$_.TargetName;Type=[uint32]$_.Type}})}
DeleteCredential={param($target,$type)Initialize-MsPrivateCredentialNative;[MsPrivate.CredentialNative]::Delete($target,[uint32]$type)}
}
}
function Get-MsPrivateStableId {param([string]$Text)$sha=[Security.Cryptography.SHA256]::Create();try{$bytes=[Text.Encoding]::UTF8.GetBytes($Text);$hash=$sha.ComputeHash($bytes);'credential-'+(([BitConverter]::ToString($hash)).Replace('-','').Substring(0,16).ToLowerInvariant())}finally{$sha.Dispose()}}
function Get-MsPrivateAccountPlan {
param([ValidateSet('Consumer','AllAppAccounts')][string]$Scope='AllAppAccounts',[hashtable]$Backend=(New-MsPrivateAccountBackend))
$catalog=Get-MsPrivateAccountCatalog;$packages=@(& $Backend.ListPackages)
foreach($item in @($catalog.Packages|Where-Object{$_.Scope-eq'Consumer'-or$Scope-eq'AllAppAccounts'})){
$matches=@($packages|Where-Object Name -eq $item.Name)
if($matches.Count-gt1){throw "Ambiguous package inventory: $($item.Name)"}
[pscustomobject]@{Id=$item.Id;Kind='Package';Label=$item.Label;Status=if($matches.Count){'WouldReset'}else{'Unavailable'};Key=if($matches.Count){$matches[0].PackageFullName}else{$null};CredentialType=$null;Impact=$item.Impact}
}
foreach($credential in @(& $Backend.ListCredentials)){
if(@($catalog.CredentialTargetPatterns|Where-Object{[string]$credential.TargetName-match$_}).Count){
[pscustomobject]@{Id=(Get-MsPrivateStableId ([string]$credential.TargetName+':'+[string]$credential.Type));Kind='Credential';Label='Stored Microsoft application credential';Status='WouldDelete';Key=[string]$credential.TargetName;CredentialType=[uint32]$credential.Type;Impact='Deletes a matching Credential Manager entry. The secret is not read or written to the report.'}
}
}
}
function Save-MsPrivateAccountReport {param($Report,[Parameter(Mandatory)][string]$Path)Write-MsPrivateJsonAtomic -InputObject $Report -Path $Path -Depth 8}
function Invoke-MsPrivateAccountPurge {
param([ValidateSet('Consumer','AllAppAccounts')][string]$Scope='AllAppAccounts',[Parameter(Mandatory)][hashtable]$Backend,[Parameter(Mandatory)][scriptblock]$Save)
$plan=@(Get-MsPrivateAccountPlan -Scope $Scope -Backend $Backend)
$entries=@($plan|Where-Object {$_.Status -in @('WouldReset','WouldDelete')}|ForEach-Object{[pscustomobject]@{Id=$_.Id;Kind=$_.Kind;Label=$_.Label;Status='Pending'}})
$report=[pscustomobject]@{Schema=1;Kind='MsPrivateAccounts';CreatedUtc=[DateTime]::UtcNow.ToString('o');Scope=$Scope;Status='Prepared';Entries=$entries};& $Save $report
try{
foreach($item in @($plan|Where-Object {$_.Status -in @('WouldReset','WouldDelete')})){$entry=$entries|Where-Object Id -eq $item.Id;$entry.Status='Working';& $Save $report;if($item.Kind-eq'Package'){& $Backend.ResetPackage $item.Key;$entry.Status='Reset'}else{& $Backend.DeleteCredential $item.Key $item.CredentialType;$entry.Status='Deleted'};& $Save $report}
$report.Status='Completed';& $Save $report;$report
}catch{$report.Status='PartialFailure';& $Save $report;throw}
}
Export-ModuleMember -Function Get-MsPrivateAccountCatalog,New-MsPrivateAccountBackend,Get-MsPrivateAccountPlan,Save-MsPrivateAccountReport,Invoke-MsPrivateAccountPurge
+39
View File
@@ -0,0 +1,39 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Invoke-MsPrivateGroupPlan {
param(
[Parameter(Mandatory)][object[]]$Groups,
[scriptblock]$OnProgress,
[string]$ReportPath
)
$seen=@{};$known=@{}
foreach($group in $Groups){
$id=[string]$group.Id
if($id-notmatch'^[A-Z][A-Za-z0-9-]*$'-or$seen.ContainsKey($id)-or$group.Action-isnot[scriptblock]){throw "Invalid or duplicate operation group: $id"}
foreach($dependency in @($group.DependsOn)){if(-not$known.ContainsKey([string]$dependency)){throw "Operation dependency must reference an earlier group: $id/$dependency"}}
$seen[$id]=$true;$known[$id]=$true
}
$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}
$save={if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8}}.GetNewClosure();&$save
foreach($group in $Groups){
$entry=@($entries | Where-Object { $_.Id -ceq [string]$group.Id })[0]
$blocked=@($entry.DependsOn | Where-Object {
$dependency=[string]$_
$dependencyEntry=@($entries | Where-Object { $_.Id -ceq $dependency })
$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}
$entry.Status='Running';&$save;if($OnProgress){&$OnProgress $entry}
try{& $group.Action|Out-Host;$entry.Status='Succeeded'}catch{$entry.Status='Failed';$entry.Error=$_.Exception.Message}
&$save
}
$failed=@($entries | Where-Object { $_.Status -eq 'Failed' }).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
}
Export-ModuleMember -Function Invoke-MsPrivateGroupPlan
+41
View File
@@ -0,0 +1,41 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-MsPrivateJsonAtomic {
[CmdletBinding()]
param(
[Parameter(Mandatory)]$InputObject,
[Parameter(Mandatory)][string]$Path,
[ValidateRange(2, 100)][int]$Depth = 12
)
$fullPath = [IO.Path]::GetFullPath($Path)
$parent = [IO.Path]::GetDirectoryName($fullPath)
[IO.Directory]::CreateDirectory($parent) | Out-Null
$tempPath = $fullPath + '.' + [guid]::NewGuid().ToString('N') + '.tmp'
$backupPath = $tempPath + '.previous'
try {
$json = $InputObject | ConvertTo-Json -Depth $Depth
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($json)
$stream = [IO.FileStream]::new($tempPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush($true)
} finally {
$stream.Dispose()
}
if ([IO.File]::Exists($fullPath)) {
[IO.File]::Replace($tempPath, $fullPath, $backupPath)
try { [IO.File]::Delete($backupPath) }
catch { Write-Warning "JSON saved, but its previous copy remains at $backupPath" }
} else {
[IO.File]::Move($tempPath, $fullPath)
}
} finally {
if ([IO.File]::Exists($tempPath)) { [IO.File]::Delete($tempPath) }
}
}
Export-ModuleMember -Function Write-MsPrivateJsonAtomic
+75
View File
@@ -0,0 +1,75 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
function Get-MsPrivatePresetCatalog {
param([string]$Path=(Join-Path $PSScriptRoot '../config/presets.json'))
$catalog=Get-Content -LiteralPath $Path -Raw|ConvertFrom-Json
if($catalog.Schema-ne2-or-not@($catalog.Presets).Count){throw 'Unsupported or empty preset catalog.'}
if([int]$catalog.FreshInstallMaxAgeDays-lt1-or[int]$catalog.FreshInstallMaxAgeDays-gt30){throw 'Fresh-install age threshold must be between 1 and 30 days.'}
$ids=@{}
foreach($p in @($catalog.Presets)){
$id=[string]$p.Id
if($id-notmatch'^[A-Z][A-Za-z0-9-]*$'-or$ids.ContainsKey($id)){throw "Invalid or duplicate preset ID: $id"}
if([string]::IsNullOrWhiteSpace([string]$p.Label)-or[string]::IsNullOrWhiteSpace([string]$p.Description)){throw "Preset requires a label and description: $id"}
if($p.Profile-notin@('Standard','Strict')-or$p.Updates-notin@('Keep','Automatic','Notify','Manual')-or$p.Network-notin@('Observe','BlockOptionalTelemetry','Isolate')){throw "Invalid preset mode: $id"}
if([int]$p.RollbackMinutes-lt2-or[int]$p.RollbackMinutes-gt60){throw "Invalid preset rollback interval: $id"}
$browsers=@($p.Browsers);if(@($browsers|Where-Object{$_-notin@('Zen','Helium','Tor')}).Count-or@($browsers|Select-Object -Unique).Count-ne$browsers.Count){throw "Invalid preset browser selection: $id"}
foreach($name in @('Services','Accounts','RemoveBundled','RemovePersonal','RemoveStore','RemoveOneDrive','RemoveEdge','Recall','RemoveRecall','Cleanup','PrivacyCleanup','CleanupSchedule','Lock','InstallMullvad','InstallVeraCrypt','DisableSensors')){if($p.$name-isnot[bool]){throw "Preset boolean is invalid: $id/$name"}}
if($p.RemoveRecall-and-not$p.Recall){throw "Preset cannot remove Recall without enabling its deletion workflow: $id"}
if($p.RemoveEdge-and-not$browsers.Count){throw "Preset removing Edge requires a replacement browser: $id"}
if($id-in@('FreshInstall','Medium','Strong','Maximum')-and(-not$p.RemovePersonal-or-not$p.RemoveStore-or-not$p.RemoveOneDrive-or-not$p.RemoveEdge-or-not$p.Recall-or-not$p.Accounts-or-not$p.Services)){throw "Aggressive preset is incomplete: $id"}
if($id-eq'EstablishedSafe'-and($p.Services-or$p.Accounts-or$p.RemoveBundled-or$p.RemovePersonal-or$p.RemoveStore-or$p.RemoveOneDrive-or$p.RemoveEdge-or$p.Recall-or$p.RemoveRecall-or$p.Network-ne'Observe'-or$p.Cleanup-or$p.PrivacyCleanup-or$p.CleanupSchedule-or$p.Lock-or$p.InstallMullvad-or$p.InstallVeraCrypt-or$p.DisableSensors-or@($p.Browsers).Count)){throw 'EstablishedSafe contains a destructive or installation action.'}
$ids[$id]=$true
}
foreach($name in @('DefaultFreshPreset','DefaultEstablishedPreset')){if(-not$ids.ContainsKey([string]$catalog.$name)){throw "Preset catalog default is missing: $name"}}
$catalog
}
function Get-MsPrivatePreset {
param([Parameter(Mandatory)][string]$Id,[string]$Path=(Join-Path $PSScriptRoot '../config/presets.json'))
$catalog=Get-MsPrivatePresetCatalog -Path $Path
$found=@($catalog.Presets|Where-Object Id -CEQ $Id)
if($found.Count-ne1){throw "Unknown preset: $Id"}
$found[0]
}
function Resolve-MsPrivateRecallDisposition {
param([bool]$Requested,$Plan)
if(-not$Requested){return [pscustomobject]@{Requested=$false;Applicable=$false;Conflict=$false;Skipped=@()}}
if($null-eq$Plan){throw 'A requested Recall workflow requires a plan.'}
$items=@($Plan.Items)
if(-not$items.Count){throw 'A requested Recall workflow returned an empty plan.'}
$unknown=@($items|Where-Object {$_.Status -notin @('WouldChange','Configured','Unsupported','Unavailable','Conflict')})
if($unknown.Count){throw 'Recall plan contains an unknown status.'}
$conflicts=@($items|Where-Object {$_.Status -eq 'Conflict'})
$skipped=@($items|Where-Object {$_.Status -in @('Unsupported','Unavailable')}|ForEach-Object{"$($_.Id): $($_.Status) on this Windows edition/build"})
[pscustomobject]@{Requested=$true;Applicable=[bool](-not$conflicts.Count-and-not$skipped.Count);Conflict=[bool]$conflicts.Count;Skipped=$skipped}
}
function Get-MsPrivateInstallContext {
param(
[string]$Path=(Join-Path $PSScriptRoot '../config/presets.json'),
[datetime]$NowUtc=[DateTime]::UtcNow,
[scriptblock]$Probe={
$os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$profile=Get-Item -LiteralPath $env:USERPROFILE -Force -ErrorAction Stop
[pscustomobject]@{InstallDate=$os.InstallDate;ProfileCreationUtc=$profile.CreationTimeUtc}
}
)
$catalog=Get-MsPrivatePresetCatalog -Path $Path;$threshold=[int]$catalog.FreshInstallMaxAgeDays
try{$evidence=& $Probe}catch{return [pscustomobject]@{Classification='Unknown';RecommendedPreset=[string]$catalog.DefaultEstablishedPreset;ThresholdDays=$threshold;InstallAgeDays=$null;ProfileAgeDays=$null;Reason='Installation age could not be read, so the conservative established-PC preset was selected.'}}
try{
$installed=([datetime]$evidence.InstallDate).ToUniversalTime();$profileCreated=([datetime]$evidence.ProfileCreationUtc).ToUniversalTime();$now=$NowUtc.ToUniversalTime()
$installAge=[int][Math]::Floor(($now-$installed).TotalDays);$profileAge=[int][Math]::Floor(($now-$profileCreated).TotalDays)
if($installAge-lt0-or$profileAge-lt0){throw 'Future installation timestamp.'}
}catch{return [pscustomobject]@{Classification='Unknown';RecommendedPreset=[string]$catalog.DefaultEstablishedPreset;ThresholdDays=$threshold;InstallAgeDays=$null;ProfileAgeDays=$null;Reason='Installation timestamps were invalid, so the conservative established-PC preset was selected.'}}
$fresh=$installAge-le$threshold-and$profileAge-le$threshold
[pscustomobject]@{
Classification=if($fresh){'FreshInstall'}else{'Established'}
RecommendedPreset=if($fresh){[string]$catalog.DefaultFreshPreset}else{[string]$catalog.DefaultEstablishedPreset}
ThresholdDays=$threshold;InstallAgeDays=$installAge;ProfileAgeDays=$profileAge
Reason=if($fresh){"Windows and the current user profile are at most $threshold days old."}else{"Windows or the current user profile is older than $threshold days; broad destructive choices were not selected automatically."}
}
}
Export-ModuleMember -Function Get-MsPrivatePresetCatalog,Get-MsPrivatePreset,Resolve-MsPrivateRecallDisposition,Get-MsPrivateInstallContext
+98
View File
@@ -0,0 +1,98 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-MsPrivateServiceCatalog {
param([string]$Path=(Join-Path $PSScriptRoot '../config/services.json'))
$catalog=Get-Content -LiteralPath $Path -Raw|ConvertFrom-Json
if($catalog.Schema-ne 1){throw 'Unsupported service catalog schema.'}
if(-not@($catalog.Services).Count-or-not@($catalog.ProtectedServices).Count){throw 'Service catalog must contain service and protected-service entries.'}
$protected=@{};foreach($service in @($catalog.ProtectedServices)){$name=[string]$service;if($name-notmatch'^[A-Za-z0-9_]+$'-or$protected.ContainsKey($name)){throw "Invalid or duplicate protected service: $name"};$protected[$name]=$true}
$ids=@{};$names=@{};foreach($item in @($catalog.Services)){
$id=[string]$item.Id;$name=[string]$item.Name
if($id-notmatch'^[a-z0-9][a-z0-9-]*$'-or$ids.ContainsKey($id)){throw "Invalid or duplicate service ID: $id"}
if($name-notmatch'^[A-Za-z0-9_]+$'-or$names.ContainsKey($name)){throw "Invalid or duplicate service template: $name"}
if($item.Standard-notin@('Keep','Disabled')-or$item.Strict-notin@('Keep','Disabled')){throw "Invalid service profile value: $id"}
if($protected.ContainsKey($name)){throw "Protected service entered the disable catalog: $name"}
$source=$null;if(-not[uri]::TryCreate([string]$item.Source,[UriKind]::Absolute,[ref]$source)-or$source.Scheme-ne'https'){throw "Service requires an HTTPS source: $id"}
$ids[$id]=$true;$names[$name]=$true
}
$catalog
}
function New-MsPrivateServiceBackend {
@{
ReadTemplate={param($name)
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine','Registry64');try{$key=$base.OpenSubKey("SYSTEM\CurrentControlSet\Services\$name",$false);try{if(-not$key){return [pscustomobject]@{Exists=$false;StartExists=$false;Start=$null;FlagsExists=$false;Flags=$null}}
$values=@($key.GetValueNames());[pscustomobject]@{Exists=$true;StartExists=$values-contains'Start';Start=if($values-contains'Start'){[int]$key.GetValue('Start')}else{$null};FlagsExists=$values-contains'UserServiceFlags';Flags=if($values-contains'UserServiceFlags'){[int]$key.GetValue('UserServiceFlags')}else{$null}}
}finally{if($key){$key.Dispose()}}}finally{$base.Dispose()}}
WriteTemplate={param($name,$state)
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine','Registry64');try{$key=$base.OpenSubKey("SYSTEM\CurrentControlSet\Services\$name",$true);if(-not$key){throw "Service template disappeared: $name"};try{if($state.StartExists){$key.SetValue('Start',[int]$state.Start,[Microsoft.Win32.RegistryValueKind]::DWord)}else{$key.DeleteValue('Start',$false)};if($state.FlagsExists){$key.SetValue('UserServiceFlags',[int]$state.Flags,[Microsoft.Win32.RegistryValueKind]::DWord)}else{$key.DeleteValue('UserServiceFlags',$false)}}finally{$key.Dispose()}}finally{$base.Dispose()}}
ReadInstances={param($name,$instances) $pattern=if($instances){$name+'_*'}else{$name};@(Get-Service -Name $pattern -ErrorAction SilentlyContinue|ForEach-Object{[pscustomobject]@{Name=$_.Name;Status=[string]$_.Status}})}
StopInstance={param($name) Stop-Service -Name $name -Force -ErrorAction Stop}
StartInstance={param($name) Start-Service -Name $name -ErrorAction Stop}
}
}
function Test-MsPrivateServiceStateEqual {param($Left,$Right) return $Left.Exists -eq $Right.Exists -and $Left.StartExists -eq $Right.StartExists -and $Left.Start -eq $Right.Start -and $Left.FlagsExists -eq $Right.FlagsExists -and $Left.Flags -eq $Right.Flags}
function Assert-MsPrivateServiceEntries {
param([object[]]$Entries,[string]$Context='service data',[switch]$Journal)
$catalog=Get-MsPrivateServiceCatalog
$byId=@{};foreach($item in @($catalog.Services)){$byId[[string]$item.Id]=$item}
$seen=@{}
foreach($entry in @($Entries)){
if(-not$entry.PSObject.Properties['Id']-or-not$entry.PSObject.Properties['Name']){throw "Invalid $Context entry."}
$id=[string]$entry.Id;$name=[string]$entry.Name
if(-not$byId.ContainsKey($id)-or$name-ne[string]$byId[$id].Name){throw "Unknown or mismatched $Context entry: $id"}
if($seen.ContainsKey($id)){throw "Duplicate $Context entry: $id"};$seen[$id]=$true
if($Journal){
foreach($property in @('Before','After','BeforeInstances')){if(-not$entry.PSObject.Properties[$property]){throw "Invalid $Context entry for ${id}: missing $property"}}
foreach($state in @($entry.Before,$entry.After)){
foreach($property in @('Exists','StartExists','Start','FlagsExists','Flags')){if(-not$state.PSObject.Properties[$property]){throw "Invalid $Context state for $id."}}
if($state.StartExists-and($null-eq$state.Start-or[int]$state.Start-lt0-or[int]$state.Start-gt4)){throw "Invalid $Context startup value for $id."}
}
foreach($instance in @($entry.BeforeInstances)){
if(-not$instance.PSObject.Properties['Name']-or-not$instance.PSObject.Properties['Status']){throw "Invalid $Context instance for $id."}
$valid=if([bool]$byId[$id].Instances){[string]$instance.Name-like($name+'_*')}else{[string]$instance.Name-eq$name}
if(-not$valid){throw "Mismatched $Context instance for $id."}
}
}
}
}
function Get-MsPrivateServicePlan {
param([ValidateSet('Standard','Strict')][string]$Profile='Strict',[hashtable]$Backend=(New-MsPrivateServiceBackend))
$catalog=Get-MsPrivateServiceCatalog
foreach($item in @($catalog.Services)){
$before=& $Backend.ReadTemplate $item.Name;$desired=[string]$item.$Profile;$status='Keep';$reason='Profile keeps this service unchanged.'
if($desired -eq 'Disabled'){
if(-not$before.Exists){$status='Unavailable';$reason='Service template is absent on this Windows build.'}
elseif($before.StartExists -and $before.Start -eq 4 -and (-not $item.SetUserServiceFlags -or ($before.FlagsExists -and $before.Flags -eq 0))){$status='Configured';$reason='Template is disabled; current instances still require status verification.'}
else{$status='WouldChange';$reason='Template will be disabled and matching running instances stopped.'}
}
[pscustomobject]@{Id=$item.Id;Name=$item.Name;Instances=[bool]$item.Instances;SetUserServiceFlags=[bool]$item.SetUserServiceFlags;Status=$status;Before=$before;Desired=$desired;Impact=$item.Impact;Source=$item.Source;Reason=$reason}
}
}
function Save-MsPrivateServiceJournal {param($Journal,[Parameter(Mandatory)][string]$Path)Write-MsPrivateJsonAtomic -InputObject $Journal -Path $Path -Depth 10}
function Invoke-MsPrivateServiceApply {
param([Parameter(Mandatory)][object[]]$Plan,[Parameter(Mandatory)][hashtable]$Backend,[Parameter(Mandatory)][scriptblock]$Save)
Assert-MsPrivateServiceEntries -Entries $Plan -Context 'service plan'
$entries=@($Plan|Where-Object Status -eq 'WouldChange'|ForEach-Object{
$instances=@(& $Backend.ReadInstances $_.Name $_.Instances)
$after=[pscustomobject]@{Exists=$true;StartExists=$true;Start=4;FlagsExists=if($_.SetUserServiceFlags){$true}else{[bool]$_.Before.FlagsExists};Flags=if($_.SetUserServiceFlags){0}else{$_.Before.Flags}}
[pscustomobject]@{Id=$_.Id;Name=$_.Name;Instances=$_.Instances;Before=$_.Before;BeforeInstances=$instances;After=$after;Status='Pending'}
})
$journal=[pscustomobject]@{Schema=1;Kind='MsPrivateServices';CreatedUtc=[DateTime]::UtcNow.ToString('o');Status='Prepared';Entries=$entries};& $Save $journal
try{foreach($entry in $entries){$current=& $Backend.ReadTemplate $entry.Name;if(-not(Test-MsPrivateServiceStateEqual $current $entry.Before)){throw "Service state changed since preview: $($entry.Name)"};$entry.Status='Writing';& $Save $journal;& $Backend.WriteTemplate $entry.Name $entry.After;foreach($instance in @(& $Backend.ReadInstances $entry.Name $entry.Instances)){if($instance.Status -eq 'Running'){& $Backend.StopInstance $instance.Name}};$readback=& $Backend.ReadTemplate $entry.Name;if(-not(Test-MsPrivateServiceStateEqual $readback $entry.After)){throw "Service disable readback failed: $($entry.Name)"};$entry.Status='Written';& $Save $journal};$journal.Status='Applied';& $Save $journal;$journal}catch{$journal.Status='ApplyFailed';& $Save $journal;throw}
}
function Restore-MsPrivateServices {
param([Parameter(Mandatory)]$Journal,[Parameter(Mandatory)][hashtable]$Backend,[Parameter(Mandatory)][scriptblock]$Save)
if($Journal.Schema -ne 1 -or $Journal.Kind -ne 'MsPrivateServices'){throw 'Invalid service journal.'};Assert-MsPrivateServiceEntries -Entries @($Journal.Entries) -Context 'service journal' -Journal;$problems=0;$entries=@($Journal.Entries);[array]::Reverse($entries)
foreach($entry in $entries){try{$current=& $Backend.ReadTemplate $entry.Name;if(Test-MsPrivateServiceStateEqual $current $entry.Before){$entry.Status='Restored'}elseif(Test-MsPrivateServiceStateEqual $current $entry.After){& $Backend.WriteTemplate $entry.Name $entry.Before;foreach($instance in @($entry.BeforeInstances|Where-Object Status -eq 'Running')){try{& $Backend.StartInstance $instance.Name}catch{}};if(-not(Test-MsPrivateServiceStateEqual (& $Backend.ReadTemplate $entry.Name) $entry.Before)){throw 'Restore readback failed.'};$entry.Status='Restored'}else{$entry.Status='Conflict';$problems++}}catch{$entry.Status='RestoreFailed';$problems++};& $Save $Journal};$Journal.Status=if($problems){'RestoreIncomplete'}else{'Restored'};& $Save $Journal;$Journal
}
Export-ModuleMember -Function Get-MsPrivateServiceCatalog,New-MsPrivateServiceBackend,Get-MsPrivateServicePlan,Save-MsPrivateServiceJournal,Invoke-MsPrivateServiceApply,Restore-MsPrivateServices,Test-MsPrivateServiceStateEqual
+61 -17
View File
@@ -2,18 +2,21 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
function Get-WindBrowserCatalog { function Get-WindBrowserCatalog {
@(Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/apps.json') -Raw | ConvertFrom-Json) $items = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/apps.json') -Raw | ConvertFrom-Json
foreach ($item in $items) { $item }
} }
function Test-WindProtectedApp { function Test-WindProtectedApp {
param([Parameter(Mandatory)]$App) param([Parameter(Mandatory)]$App,[switch]$AllowStoreRemoval)
if($AllowStoreRemoval -and $App.Kind -eq 'Appx' -and $App.Name -in @('Microsoft.WindowsStore','Microsoft.StorePurchaseApp')){return $false}
if ($App.Protected) { return $true } if ($App.Protected) { return $true }
# Removal is deliberately unavailable for infrastructure and known security tools. # Removal is deliberately unavailable for infrastructure and known security tools.
$text = $App.Name + ' ' + $App.Publisher $text = $App.Name + ' ' + $App.Publisher
return $text -match '(?i)WebView|DesktopAppInstaller|WindowsStore|StorePurchaseApp|SecHealth|SecurityHealth|AAD.Broker|AccountsControl|CloudExperienceHost|ShellExperienceHost|StartMenuExperienceHost|MicrosoftWindows\.|Microsoft\.Windows\.|Microsoft\.UI\.|VCLibs|NET\.Native|WinAppRuntime|WindowsAppRuntime|WindowsTerminal|WindowsSubsystem|GamingServices|XboxIdentityProvider|Xbox.TCUI|XboxGameOverlay|XboxGamingOverlay|HEIF|HEVC|WebMediaExtension|AV1VideoExtension|VP9VideoExtension|Visual C\+\+|\.NET|SDK|Driver|Firmware|Update for|Hotfix|Redistributable|Windows.*(Runtime|Kit)|Defender|Security|Antivirus|Anti-malware|Bitdefender|ESET|Kaspersky|Malwarebytes|Norton|McAfee|Avast|AVG |Sophos|CrowdStrike|SentinelOne|Zillya|Mullvad|VeraCrypt|BitLocker|1Password|KeePass|Bitwarden|Zen Browser|Helium|Tor Browser|PowerShell|NVIDIA|Realtek|Advanced Micro Devices|Intel\(R\)' return $text -match '(?i)WebView|DesktopAppInstaller|WindowsStore|StorePurchaseApp|SecHealth|SecurityHealth|AAD.Broker|AccountsControl|CloudExperienceHost|ShellExperienceHost|StartMenuExperienceHost|MicrosoftWindows\.|Microsoft\.UI\.|VCLibs|NET\.Native|WinAppRuntime|WindowsAppRuntime|WindowsTerminal|WindowsSubsystem|GamingServices|XboxIdentityProvider|Xbox.TCUI|XboxGameOverlay|XboxGamingOverlay|HEIF|HEVC|WebMediaExtension|AV1VideoExtension|VP9VideoExtension|Visual C\+\+|\.NET|SDK|Driver|Firmware|Update for|Hotfix|Redistributable|Windows.*(Runtime|Kit)|Defender|Security|Antivirus|Anti-malware|Bitdefender|ESET|Kaspersky|Malwarebytes|Norton|McAfee|Avast|AVG |Sophos|CrowdStrike|SentinelOne|Zillya|Mullvad|VeraCrypt|BitLocker|2FAGuard|Authenticator|Authy|Yubi|F-Secure|Emsisoft|Trend Micro|1Password|KeePass|Bitwarden|Zen Browser|Helium|Tor Browser|PowerShell|NVIDIA|Realtek|Advanced Micro Devices|Intel\(R\)'
} }
function Initialize-WindAppx { function Initialize-WindAppx {
if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) { return }
if ($PSVersionTable.PSEdition -eq 'Core') { if ($PSVersionTable.PSEdition -eq 'Core') {
Import-Module Appx -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop Import-Module Appx -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop
} else { Import-Module Appx -ErrorAction Stop } } else { Import-Module Appx -ErrorAction Stop }
@@ -22,7 +25,9 @@ function Initialize-WindAppx {
function Get-WindAppInventory { function Get-WindAppInventory {
Initialize-WindAppx Initialize-WindAppx
# Current user only. Do not query Win32_Product: enumeration can trigger MSI repairs. # Current user only. Do not query Win32_Product: enumeration can trigger MSI repairs.
foreach ($p in @(Get-AppxPackage -PackageTypeFilter Main,Bundle -ErrorAction Stop)) { $packages = @(Get-AppxPackage -PackageTypeFilter Main,Bundle -ErrorAction Stop)
$parents = @(Select-WindAppxParents $packages)
foreach ($p in $parents) {
[pscustomobject]@{ [pscustomobject]@{
Id = 'appx:' + $p.PackageFullName; Name = [string]$p.Name Id = 'appx:' + $p.PackageFullName; Name = [string]$p.Name
Version = [string]$p.Version; Publisher = [string]$p.Publisher Version = [string]$p.Version; Publisher = [string]$p.Publisher
@@ -31,6 +36,7 @@ function Get-WindAppInventory {
} }
} }
$uninstall = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' $uninstall = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
$sharedUserEntries = @{}
foreach ($hive in @('LocalMachine','CurrentUser')) { foreach ($hive in @('LocalMachine','CurrentUser')) {
foreach ($view in @('Registry64','Registry32')) { foreach ($view in @('Registry64','Registry32')) {
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive,$view) $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive,$view)
@@ -45,6 +51,12 @@ function Get-WindAppInventory {
$display = [string]$key.GetValue('DisplayName') $display = [string]$key.GetValue('DisplayName')
if ([string]::IsNullOrWhiteSpace($display)) { continue } if ([string]::IsNullOrWhiteSpace($display)) { continue }
if (-not $key.GetValue('UninstallString')) { continue } if (-not $key.GetValue('UninstallString')) { continue }
# HKCU uninstall keys may be shared between 32/64-bit views.
if ($hive -eq 'CurrentUser') {
$fingerprint=@($name,$display,[string]$key.GetValue('DisplayVersion'),[string]$key.GetValue('Publisher'),[string]$key.GetValue('UninstallString')) | ConvertTo-Json -Compress
if ($sharedUserEntries.ContainsKey($fingerprint)) { continue }
$sharedUserEntries[$fingerprint]=$true
}
[pscustomobject]@{ [pscustomobject]@{
Id = "desktop:$hive/$view/$name"; Name = $display Id = "desktop:$hive/$view/$name"; Name = $display
Version = [string]$key.GetValue('DisplayVersion'); Publisher = [string]$key.GetValue('Publisher') Version = [string]$key.GetValue('DisplayVersion'); Publisher = [string]$key.GetValue('Publisher')
@@ -60,15 +72,26 @@ function Get-WindAppInventory {
} }
} }
function Select-WindAppxParents {
param([object[]]$Packages)
foreach ($group in @($Packages | Group-Object PackageFamilyName)) {
$bundles=@($group.Group | Where-Object IsBundle)
if ($bundles.Count) { $bundles } else { $group.Group }
}
}
function Get-WindAppPlan { function Get-WindAppPlan {
param( param(
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Inventory, [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Inventory,
[string[]]$ExtraId = @(), [string[]]$ExcludeId = @(), [string[]]$ExtraId = @(), [string[]]$ExcludeId = @(),
[switch]$SkipBundled, [switch]$KeepOneDrive, [switch]$KeepEdge [switch]$SkipBundled, [switch]$KeepOneDrive, [switch]$KeepEdge,
[switch]$RemovePersonal, [switch]$RemoveStore
) )
$catalog = @(Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/debloat.json') -Raw | ConvertFrom-Json) $decodedCatalog = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/debloat.json') -Raw | ConvertFrom-Json
$catalog = @($decodedCatalog)
$inventoryIds = @($Inventory | ForEach-Object { $_.Id })
foreach ($id in @($ExtraId + $ExcludeId)) { foreach ($id in @($ExtraId + $ExcludeId)) {
if ($Inventory.Id -notcontains $id) { throw "Selection no longer installed: $id. Refresh the app list." } if ($inventoryIds -notcontains $id) { throw "Selection no longer installed: $id. Refresh the app list." }
} }
foreach ($app in $Inventory) { foreach ($app in $Inventory) {
$reason = 'Personal app; opt in to remove' $reason = 'Personal app; opt in to remove'
@@ -76,21 +99,28 @@ function Get-WindAppPlan {
if (-not $SkipBundled -and $app.Kind -eq 'Appx' -and $catalog.Name -contains $app.Name) { if (-not $SkipBundled -and $app.Kind -eq 'Appx' -and $catalog.Name -contains $app.Name) {
$selected = $true; $reason = 'Bundled/promotional catalog' $selected = $true; $reason = 'Bundled/promotional catalog'
} }
if($RemovePersonal){$selected=$true;$reason='Aggressive preset: all removable applications'}
if ($app.Kind -eq 'Desktop' -and $app.Name -eq 'Microsoft OneDrive') { if ($app.Kind -eq 'Desktop' -and $app.Name -eq 'Microsoft OneDrive') {
$selected = -not $KeepOneDrive; $reason = 'OneDrive: sync stops; cloud-only files stay online' $selected = -not $KeepOneDrive; $reason = 'OneDrive: sync stops; cloud-only files stay online'
} }
if ($app.Kind -eq 'Desktop' -and $app.Name -eq 'Microsoft Edge') { if ($app.Kind -eq 'Desktop' -and $app.Name -eq 'Microsoft Edge') {
$selected = -not $KeepEdge; $reason = 'Edge: supported uninstaller only; replacement browser required' $selected = -not $KeepEdge; $reason = 'Edge: supported uninstaller only; replacement browser required'
} }
$protected = Test-WindProtectedApp $app $storeRemovalAllowed=[bool]($RemoveStore-and$app.Kind-eq'Appx'-and$app.Name-in@('Microsoft.WindowsStore','Microsoft.StorePurchaseApp'))
if($storeRemovalAllowed){$selected=$true;$reason='Aggressive preset: Microsoft Store application'}
$protected = Test-WindProtectedApp $app -AllowStoreRemoval:$storeRemovalAllowed
if ($protected) { $selected = $false; $reason = 'Protected: system component, dependency or security tool' } if ($protected) { $selected = $false; $reason = 'Protected: system component, dependency or security tool' }
if ($ExcludeId -contains $app.Id) { $selected = $false; $reason = 'Kept by user' } if ($ExcludeId -contains $app.Id) { $selected = $false; $reason = 'Kept by user' }
$status = if ($selected) { 'Remove' } else { 'Keep' } $status = if ($selected) { 'Remove' } else { 'Keep' }
# Exact name/scope must resolve uniquely before delegating to WinGet. # Exact name/scope must resolve uniquely before delegating to WinGet.
if ($selected -and $app.Kind -eq 'Desktop' -and @($Inventory | Where-Object { $_.Kind -eq 'Desktop' -and $_.Name -ceq $app.Name -and $_.Scope -eq $app.Scope }).Count -ne 1) { if ($selected -and $app.Kind -eq 'Desktop' -and @($Inventory | Where-Object { $_.Kind -eq 'Desktop' -and $_.Name -ceq $app.Name -and $_.Scope -eq $app.Scope }).Count -ne 1) {
$status = 'Blocked'; $reason = 'Ambiguous desktop registration; use Windows Installed apps' if($RemovePersonal-and$ExtraId-notcontains$app.Id){
$status='Keep';$protected=$true;$reason='Skipped: duplicate desktop name/scope cannot be removed unambiguously'
}else{
$status = 'Blocked'; $reason = 'Ambiguous desktop registration; use Windows Installed apps'
}
} }
[pscustomobject]@{ Id=$app.Id; Name=$app.Name; Version=$app.Version; Publisher=$app.Publisher; Kind=$app.Kind; Scope=$app.Scope; Target=$app.Target; Protected=$protected; Status=$status; Reason=$reason } [pscustomobject]@{ Id=$app.Id; Name=$app.Name; Version=$app.Version; Publisher=$app.Publisher; Kind=$app.Kind; Scope=$app.Scope; Target=$app.Target; Protected=$protected; StoreRemovalAllowed=$storeRemovalAllowed; Status=$status; Reason=$reason }
} }
} }
@@ -102,6 +132,19 @@ function Invoke-WindWinget {
return [int]$LASTEXITCODE return [int]$LASTEXITCODE
} }
function Set-WindRemovalGroups {
param([hashtable]$Options,[object[]]$Inventory,[bool]$Bundled,[bool]$OneDrive,[bool]$Edge)
$catalog=Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/debloat.json') -Raw | ConvertFrom-Json
$reset=@($Inventory | Where-Object {
($Options.SkipBundled -eq $Bundled -and $_.Kind -eq 'Appx' -and $catalog.Name -contains $_.Name) -or
($Options.KeepOneDrive -eq $OneDrive -and $_.Name -eq 'Microsoft OneDrive') -or
($Options.KeepEdge -eq $Edge -and $_.Name -eq 'Microsoft Edge')
} | ForEach-Object {$_.Id})
$Options.ExtraId=@($Options.ExtraId | Where-Object {$reset -notcontains $_})
$Options.ExcludeId=@($Options.ExcludeId | Where-Object {$reset -notcontains $_})
$Options.SkipBundled=-not $Bundled; $Options.KeepOneDrive=-not $OneDrive; $Options.KeepEdge=-not $Edge
}
function New-WindAppBackend { function New-WindAppBackend {
@{ @{
Inventory = { @(Get-WindAppInventory) } Inventory = { @(Get-WindAppInventory) }
@@ -123,7 +166,7 @@ function New-WindAppBackend {
Remove-AppxPackage -Package $app.Target -ErrorAction Stop Remove-AppxPackage -Package $app.Target -ErrorAction Stop
return 0 return 0
} }
Invoke-WindWinget @('uninstall','--name',$app.Name,'--exact','--scope',$app.Scope,'--source','winget','--interactive','--disable-interactivity') Invoke-WindWinget @('uninstall','--product-code',$app.Target,'--exact','--scope',$app.Scope,'--source','winget','--interactive','--disable-interactivity')
} }
} }
} }
@@ -136,13 +179,14 @@ function Invoke-WindAppChanges {
[Parameter(Mandatory)][hashtable]$Backend, [Parameter(Mandatory)][hashtable]$Backend,
[Parameter(Mandatory)][scriptblock]$Save [Parameter(Mandatory)][scriptblock]$Save
) )
# Edge is last so an OS refusal does not prevent ordinary debloating. # Riskier OS-owned removals run last so a refusal does not prevent ordinary app cleanup.
$remove = @($Plan | Where-Object Status -eq 'Remove' | Sort-Object @{Expression={if ($_.Name -eq 'Microsoft Edge') {1} else {0}}},Name) $remove = @($Plan | Where-Object Status -eq 'Remove' | Sort-Object @{Expression={if ($_.Name -eq 'Microsoft Edge') {2}elseif($_.Name-in@('Microsoft.WindowsStore','Microsoft.StorePurchaseApp')){1}else{0}}},Name)
if (@($Plan | Where-Object Status -eq 'Blocked').Count) { throw 'Resolve blocked app selections before applying.' } if (@($Plan | Where-Object Status -eq 'Blocked').Count) { throw 'Resolve blocked app selections before applying.' }
if (@($remove | Where-Object { Test-WindProtectedApp $_ }).Count) { throw 'Protected application in removal plan.' } if (@($remove | Where-Object { Test-WindProtectedApp $_ -AllowStoreRemoval:([bool]$_.StoreRemovalAllowed) }).Count) { throw 'Protected application in removal plan.' }
if (@($Plan.Id | Select-Object -Unique).Count -ne $Plan.Count) { throw 'Duplicate app selection.' } if (@($Plan | ForEach-Object { $_.Id } | Select-Object -Unique).Count -ne $Plan.Count) { throw 'Duplicate app selection.' }
if (@($remove | Where-Object Name -eq 'Microsoft Edge').Count -and $Browser.Count -eq 0) { throw 'Select a replacement browser or keep Edge.' } if (@($remove | Where-Object Name -eq 'Microsoft Edge').Count -and $Browser.Count -eq 0) { throw 'Select a replacement browser or keep Edge.' }
$browsers = @(Get-WindBrowserCatalog | Where-Object { $Browser -contains $_.Name }) $browsers = @(Get-WindBrowserCatalog | Where-Object { $Browser -contains $_.Name })
if ($browsers.Count -ne @($Browser | Select-Object -Unique).Count) { throw 'Browser catalog is missing or duplicates a selected browser.' }
if (-not $PSCmdlet.ShouldProcess('Selected browsers and applications', 'Install browsers, then uninstall selected apps (app data may be removed)')) { if (-not $PSCmdlet.ShouldProcess('Selected browsers and applications', 'Install browsers, then uninstall selected apps (app data may be removed)')) {
return [pscustomobject]@{ Status='Preview'; Entries=@() } return [pscustomobject]@{ Status='Preview'; Entries=@() }
} }
@@ -171,7 +215,7 @@ function Invoke-WindAppChanges {
$inventory = @(& $Backend.Inventory) $inventory = @(& $Backend.Inventory)
$current = @($inventory | Where-Object Id -eq $app.Id) $current = @($inventory | Where-Object Id -eq $app.Id)
if ($current.Count -eq 0) { $entry.Status='AlreadyAbsent'; & $Save $report; continue } if ($current.Count -eq 0) { $entry.Status='AlreadyAbsent'; & $Save $report; continue }
if ($current.Count -ne 1 -or $current[0].Version -cne $app.Version -or $current[0].Name -cne $app.Name -or $current[0].Publisher -cne $app.Publisher -or $current[0].Target -cne $app.Target -or $current[0].Kind -cne $app.Kind -or $current[0].Scope -cne $app.Scope -or (Test-WindProtectedApp $current[0])) { if ($current.Count -ne 1 -or $current[0].Version -cne $app.Version -or $current[0].Name -cne $app.Name -or $current[0].Publisher -cne $app.Publisher -or $current[0].Target -cne $app.Target -or $current[0].Kind -cne $app.Kind -or $current[0].Scope -cne $app.Scope -or (Test-WindProtectedApp $current[0] -AllowStoreRemoval:([bool]$app.StoreRemovalAllowed))) {
$entry.Status='Conflict'; throw 'App changed since preview; refresh selections. No further removals performed.' $entry.Status='Conflict'; throw 'App changed since preview; refresh selections. No further removals performed.'
} }
if ($app.Kind -eq 'Desktop' -and @($inventory | Where-Object { $_.Kind -eq 'Desktop' -and $_.Name -ceq $app.Name -and $_.Scope -eq $app.Scope }).Count -ne 1) { if ($app.Kind -eq 'Desktop' -and @($inventory | Where-Object { $_.Kind -eq 'Desktop' -and $_.Name -ceq $app.Name -and $_.Scope -eq $app.Scope }).Count -ne 1) {
@@ -195,4 +239,4 @@ function Invoke-WindAppChanges {
} }
} }
Export-ModuleMember -Function Get-WindBrowserCatalog,Get-WindAppInventory,Get-WindAppPlan,Test-WindProtectedApp,New-WindAppBackend,Invoke-WindAppChanges Export-ModuleMember -Function Get-WindBrowserCatalog,Get-WindAppInventory,Get-WindAppPlan,Test-WindProtectedApp,New-WindAppBackend,Invoke-WindAppChanges,Select-WindAppxParents,Set-WindRemovalGroups
+120
View File
@@ -0,0 +1,120 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-WindCleanupRoots {
$roots=[Collections.Generic.List[string]]::new()
foreach($candidate in @([IO.Path]::GetTempPath(),[Environment]::ExpandEnvironmentVariables('%LOCALAPPDATA%\Temp'))){
if([string]::IsNullOrWhiteSpace($candidate)){continue};$full=[IO.Path]::GetFullPath($candidate).TrimEnd('\');if(-not$roots.Contains($full)){$roots.Add($full)}
}
if([Environment]::Is64BitOperatingSystem){$windowsTemp=[IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables('%WINDIR%\Temp')).TrimEnd('\');if(-not$roots.Contains($windowsTemp)){$roots.Add($windowsTemp)}}
@($roots)
}
function Get-WindBrowserCacheCatalog {
$local=[Environment]::GetFolderPath('LocalApplicationData');$roaming=[Environment]::GetFolderPath('ApplicationData');$home=[Environment]::GetFolderPath('UserProfile')
$profileRoots=[Collections.Generic.List[object]]::new()
$zen=Join-Path $roaming 'zen\Profiles'
if(Test-Path -LiteralPath $zen -PathType Container){foreach($profile in @(Get-ChildItem -LiteralPath $zen -Force -ErrorAction SilentlyContinue|Where-Object{$_.PSIsContainer-and-not($_.Attributes-band[IO.FileAttributes]::ReparsePoint)})){$profileRoots.Add([pscustomobject]@{Browser='Zen';Path=$profile.FullName;Kind='Gecko'})}}
foreach($tor in @((Join-Path $local 'Tor Browser\Browser\TorBrowser\Data\Browser\profile.default'),(Join-Path $home 'Desktop\Tor Browser\Browser\TorBrowser\Data\Browser\profile.default'),(Join-Path $env:ProgramFiles 'Tor Browser\Browser\TorBrowser\Data\Browser\profile.default'))|Select-Object -Unique){if($tor-and(Test-Path -LiteralPath $tor -PathType Container)){$profileRoots.Add([pscustomobject]@{Browser='Tor';Path=$tor;Kind='Gecko'})}}
$helium=Join-Path $local 'imput\Helium\User Data'
if(Test-Path -LiteralPath $helium -PathType Container){foreach($profile in @(Get-ChildItem -LiteralPath $helium -Force -ErrorAction SilentlyContinue|Where-Object{$_.PSIsContainer-and-not($_.Attributes-band[IO.FileAttributes]::ReparsePoint)-and($_.Name-eq'Default'-or$_.Name-eq'Guest Profile'-or$_.Name-eq'System Profile'-or$_.Name-like'Profile *')})){$profileRoots.Add([pscustomobject]@{Browser='Helium';Path=$profile.FullName;Kind='Chromium'})}}
foreach($profile in $profileRoots){
$relative=if($profile.Kind-eq'Gecko'){@('cache2','startupCache')}else{@('Cache','Code Cache','GPUCache','DawnCache','GrShaderCache','ShaderCache','Service Worker\CacheStorage')}
foreach($cache in $relative){$root=Join-Path $profile.Path $cache;if(Test-Path -LiteralPath $root -PathType Container){[pscustomobject]@{Category='BrowserCache';Root=$root;Patterns=@('*');Recurse=$true;Scheduled=$false;Description="$($profile.Browser) cache files only; profile data such as cookies, passwords, bookmarks, sessions and settings is retained"}}}
}
}
function Get-WindPrivacyCleanupCatalog {
$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 @((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'}}
[pscustomobject]@{Category='CrashReports';Root=(Join-Path $local 'CrashDumps');Patterns=@('*.dmp');Recurse=$true;Scheduled=$false;Description='Current-user application crash dumps'}
[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='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}
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'}
}
function Test-WindCleanupName {param([string]$Name,[string[]]$Patterns) foreach($pattern in $Patterns){if($Name-like$pattern){return $true}};return $false}
function Test-WindCleanupPathSafe {
param([Parameter(Mandatory)][string]$Path,[Parameter(Mandatory)][string]$Root)
$fullPath=[IO.Path]::GetFullPath($Path);$fullRoot=[IO.Path]::GetFullPath($Root).TrimEnd('\')
if(-not$fullPath.StartsWith($fullRoot+'\',[StringComparison]::OrdinalIgnoreCase)){return $false}
$cursor=[IO.Path]::GetDirectoryName($fullPath)
while($cursor-and($cursor.Length-ge$fullRoot.Length)){
try{$item=Get-Item -LiteralPath $cursor -Force -ErrorAction Stop}catch{return $false}
if($item.Attributes-band[IO.FileAttributes]::ReparsePoint){return $false}
if($cursor.Equals($fullRoot,[StringComparison]::OrdinalIgnoreCase)){return $true}
$next=[IO.Path]::GetDirectoryName($cursor);if($next-eq$cursor){break};$cursor=$next
}
return $false
}
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))
$cutoff=$Now.AddDays(-$AgeDays);$catalog=@(Get-WindPrivacyCleanupCatalog|Where-Object{$Category-contains$_.Category})
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.'}
$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}})
}
$files=[Collections.Generic.List[object]]::new();$errors=[Collections.Generic.List[object]]::new();$seen=@{}
foreach($target in $catalog){
$full=[IO.Path]::GetFullPath([string]$target.Root).TrimEnd('\');if(-not(Test-Path -LiteralPath $full -PathType Container)){continue}
try{
$rootItem=Get-Item -LiteralPath $full -Force -ErrorAction Stop;if($rootItem.Attributes-band[IO.FileAttributes]::ReparsePoint){throw 'Cleanup root is a reparse point and was skipped.'}
$queue=[Collections.Generic.Queue[string]]::new();$queue.Enqueue($full)
while($queue.Count){
$directory=$queue.Dequeue()
foreach($child in @(Get-ChildItem -LiteralPath $directory -Force -ErrorAction SilentlyContinue)){
if($child.Attributes-band[IO.FileAttributes]::ReparsePoint){continue}
if($child.PSIsContainer){if($target.Recurse){$childPath=[IO.Path]::GetFullPath($child.FullName);if($childPath.StartsWith($full+'\',[StringComparison]::OrdinalIgnoreCase)){$queue.Enqueue($childPath)}};continue}
if($child.LastWriteTime-ge$cutoff-or-not(Test-WindCleanupName $child.Name @($target.Patterns))){continue}
$path=[IO.Path]::GetFullPath($child.FullName);if(-not$path.StartsWith($full+'\',[StringComparison]::OrdinalIgnoreCase)-or$seen[$path]){continue};$seen[$path]=$true
$files.Add([pscustomobject]@{Path=$path;Root=$full;Category=[string]$target.Category;Length=[long]$child.Length;LastWriteUtc=$child.LastWriteTimeUtc.ToString('o')})
}
}
}catch{$errors.Add([pscustomobject]@{Category=$target.Category;Root=$full;Error=$_.Exception.Message})}
}
$totalBytes=[long]0;foreach($file in $files){$totalBytes+=[long]$file.Length}
[pscustomobject]@{Schema=2;CreatedUtc=[DateTime]::UtcNow.ToString('o');AgeDays=$AgeDays;CutoffUtc=$cutoff.ToUniversalTime().ToString('o');Categories=@($Category|Select-Object -Unique);FileCount=$files.Count;Bytes=$totalBytes;Files=@($files);Errors=@($errors);Exclusions='Documents, Downloads, Desktop, non-cache browser profile data, credential stores, recovery journals, event logs, Defender logs, update logs and authentication logs are excluded. Reparse points, changed files and locked files are skipped.'}
}
function Invoke-WindCleanup {
[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.'}
$catalog=@(Get-WindPrivacyCleanupCatalog|Where-Object{$Plan.Categories-contains$_.Category});$cutoff=[datetime]::Parse($Plan.CutoffUtc).ToUniversalTime()
$report=[pscustomobject]@{Schema=2;Kind='WindCleanup';CreatedUtc=[DateTime]::UtcNow.ToString('o');Categories=@($Plan.Categories);Status='Running';Deleted=0;Skipped=0;FreedBytes=[long]0;Entries=@()}
$save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $ReportPath -Depth 7}.GetNewClosure();&$save $report
foreach($entry in @($Plan.Files)){
$path=[IO.Path]::GetFullPath([string]$entry.Path);$root=[IO.Path]::GetFullPath([string]$entry.Root).TrimEnd('\');$matches=@($catalog|Where-Object{[IO.Path]::GetFullPath([string]$_.Root).TrimEnd('\')-eq$root-and$_.Category-eq$entry.Category-and(Test-WindCleanupName ([IO.Path]::GetFileName($path)) @($_.Patterns))})
$out=[pscustomobject]@{Category=$entry.Category;Path=$path;Status='Checking';Reason=$null};$report.Entries+=@($out);&$save $report
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.'}
$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}
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'}
}catch{$out.Status='Skipped';$out.Reason=$_.Exception.Message;$report.Skipped++}finally{&$save $report}
}
$report.Status='Completed';&$save $report;$report
}
function Clear-WindCurrentClipboard {
Add-Type -AssemblyName PresentationCore -ErrorAction Stop
$last=$null
for($i=0;$i-lt 5;$i++){try{[Windows.Clipboard]::Clear();return}catch{$last=$_;Start-Sleep -Milliseconds 100}}
throw "Clipboard is busy; it was not cleared. $($last.Exception.Message)"
}
function Install-WindCleanupSchedule {
param([ValidateRange(1,365)][int]$AgeDays=7,[ValidateSet('Daily','Weekly')][string]$Frequency='Daily',[Parameter(Mandatory)][string]$SourceRoot)
$runtime=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\cleanup-runtime';[IO.Directory]::CreateDirectory($runtime)|Out-Null
foreach($rel in @('Cleanup.ps1','src\Wind.Cleanup.psm1','src\Wind.Core.psm1','src\MsPrivate.IO.psm1')){$dst=Join-Path $runtime $rel;[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($dst))|Out-Null;Copy-Item -LiteralPath (Join-Path $SourceRoot $rel) -Destination $dst -Force}
$action=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ('-NoProfile -ExecutionPolicy RemoteSigned -File "{0}" -Mode Run -AgeDays {1} -Category Temp,WindReports' -f (Join-Path $runtime 'Cleanup.ps1'),$AgeDays)
$trigger=if($Frequency-eq'Daily'){New-ScheduledTaskTrigger -Daily -At '03:00'}else{New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At '03:00'}
Register-ScheduledTask -TaskName 'ms-private Safe Cleanup' -Action $action -Trigger $trigger -Description 'Deletes old regular files from allowlisted Temp paths and expired ms-private reports; security logs and recovery journals are excluded.' -Force -ErrorAction Stop|Out-Null
}
function Remove-WindCleanupSchedule {Unregister-ScheduledTask -TaskName 'ms-private Safe Cleanup' -Confirm:$false -ErrorAction SilentlyContinue}
Export-ModuleMember -Function Get-WindCleanupRoots,Get-WindBrowserCacheCatalog,Get-WindPrivacyCleanupCatalog,Get-WindCleanupPlan,Invoke-WindCleanup,Clear-WindCurrentClipboard,Install-WindCleanupSchedule,Remove-WindCleanupSchedule
+29 -20
View File
@@ -1,5 +1,6 @@
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Assert-WindMutationAllowed { function Assert-WindMutationAllowed {
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' }
@@ -16,7 +17,25 @@ function Assert-WindMutationAllowed {
} }
function Get-WindCatalog { function Get-WindCatalog {
$items = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/policies.json') -Raw | ConvertFrom-Json param([string]$Path=(Join-Path $PSScriptRoot '../config/policies.json'))
[object[]]$items = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
if (-not $items.Count) { throw 'Policy catalog is empty.' }
$ids=@{};$targets=@{}
foreach($item in $items){
$id=[string]$item.Id;$registryPath=[string]$item.Path;$name=[string]$item.Name
$hive=if($item.PSObject.Properties['Hive']){[string]$item.Hive}else{'LocalMachine'}
$target=($hive+'\'+$registryPath+'\'+$name).ToLowerInvariant()
if($id-notmatch'^[a-z0-9][a-z0-9-]*$'-or$ids.ContainsKey($id)){throw "Invalid or duplicate policy ID: $id"}
if($hive-notin@('LocalMachine','CurrentUser')){throw "Invalid policy registry hive: $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"}
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($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"}
$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"}
$exceptions=@{};foreach($exception in @($item.Exceptions)){$value=[string]$exception;if($value-notmatch'^[A-Za-z0-9_]+$'-or$exceptions.ContainsKey($value)){throw "Invalid or duplicate policy exception: $id"};$exceptions[$value]=$true}
$ids[$id]=$true;$targets[$target]=$true
}
foreach ($item in $items) { $item } foreach ($item in $items) { $item }
} }
@@ -35,11 +54,13 @@ function Get-WindContext {
} }
function New-WindRegistryStore { function New-WindRegistryStore {
# Explicit 64-bit registry view; never fall through to HKCU or a redirected view. # Explicit 64-bit registry view. The catalog selects only HKLM or the invoking user's HKCU.
@{ @{
Read = { Read = {
param($Policy) param($Policy)
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', 'Registry64') $hive=if($Policy.PSObject.Properties['Hive']){[string]$Policy.Hive}else{'LocalMachine'}
if($hive-notin@('LocalMachine','CurrentUser')){throw 'Untrusted registry hive.'}
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, 'Registry64')
try { try {
$key = $base.OpenSubKey($Policy.Path, $false) $key = $base.OpenSubKey($Policy.Path, $false)
try { try {
@@ -56,7 +77,9 @@ function New-WindRegistryStore {
} }
Write = { Write = {
param($Policy, $State) param($Policy, $State)
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', 'Registry64') $hive=if($Policy.PSObject.Properties['Hive']){[string]$Policy.Hive}else{'LocalMachine'}
if($hive-notin@('LocalMachine','CurrentUser')){throw 'Untrusted registry hive.'}
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, 'Registry64')
try { try {
if ($State.Exists) { if ($State.Exists) {
$key = $base.CreateSubKey($Policy.Path) $key = $base.CreateSubKey($Policy.Path)
@@ -110,7 +133,7 @@ function Get-WindPlan {
$status = 'Conflict'; $reason = 'Existing value is not a DWORD; it will not be overwritten.' $status = 'Conflict'; $reason = 'Existing value is not a DWORD; it will not be overwritten.'
} elseif ($before.Exists -and $before.Value -eq $desired) { $status = 'Configured' } } elseif ($before.Exists -and $before.Value -eq $desired) { $status = 'Configured' }
foreach ($exceptionName in $policy.Exceptions) { foreach ($exceptionName in $policy.Exceptions) {
$exception = & $Store.Read ([pscustomobject]@{ Path=$policy.Path; Name=$exceptionName }) $exception = & $Store.Read ([pscustomobject]@{ Hive=if($policy.PSObject.Properties['Hive']){$policy.Hive}else{'LocalMachine'}; Path=$policy.Path; Name=$exceptionName })
if ($exception.Exists -and -not [string]::IsNullOrWhiteSpace(($exception.Value -join ''))) { if ($exception.Exists -and -not [string]::IsNullOrWhiteSpace(($exception.Value -join ''))) {
$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.'
} }
@@ -131,21 +154,7 @@ function Get-WindPlan {
function Save-WindJournal { function Save-WindJournal {
param([Parameter(Mandatory)]$Journal, [Parameter(Mandatory)][string]$Path) param([Parameter(Mandatory)]$Journal, [Parameter(Mandatory)][string]$Path)
$fullPath = [IO.Path]::GetFullPath($Path) Write-MsPrivateJsonAtomic -InputObject $Journal -Path $Path -Depth 12
$tempPath = $fullPath + '.' + [guid]::NewGuid().ToString('N') + '.tmp'
$json = $Journal | ConvertTo-Json -Depth 12
# Flush the write-ahead journal before registry mutation; publish atomically on NTFS.
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($json)
$stream = [IO.FileStream]::new($tempPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try { $stream.Write($bytes,0,$bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
if ([IO.File]::Exists($fullPath)) {
# Windows PowerShell can marshal a null string argument as an empty path.
# Use a unique backup path for portable File.Replace behavior.
$backupPath = $tempPath + '.previous'
[IO.File]::Replace($tempPath, $fullPath, $backupPath)
try { [IO.File]::Delete($backupPath) }
catch { Write-Warning "Journal saved, but its previous copy remains at $backupPath" }
} else { [IO.File]::Move($tempPath, $fullPath) }
} }
function Assert-WindJournal { function Assert-WindJournal {
+39
View File
@@ -0,0 +1,39 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-WindSensorInventory {
if(-not (Get-Command Get-PnpDevice -ErrorAction SilentlyContinue)){return}
foreach($d in @(Get-PnpDevice -PresentOnly -ErrorAction Stop)){
$name=[string]$d.FriendlyName;$class=[string]$d.Class
$kind=$null
if($class -in @('Camera','Image')){$kind='Camera'}
elseif($class -eq 'AudioEndpoint' -and [string]$d.InstanceId -match '\{0\.0\.1\.'){$kind='Microphone'}
elseif($class -eq 'Media' -and $name -match '(?i)microphone|mic array|array mic'){$kind='Microphone'}
elseif($class -eq 'Sensor'){$kind='Sensor'}
if($kind){[pscustomobject]@{InstanceId=[string]$d.InstanceId;Name=$name;Class=$class;Kind=$kind;Status=[string]$d.Status;Risk=if($name -match '(?i)hello|infrared|(^|\s)IR(\s|$)'){'May disable Windows Hello sign-in'}else{'Applications lose access to this hardware device'}}}
}
}
function Get-WindSensorPlan {
param([string[]]$InstanceId=@())
$inventory=@(Get-WindSensorInventory);$seen=@{}
foreach($id in $InstanceId){if($seen[$id]){throw 'Duplicate hardware selection.'};$seen[$id]=$true;if(@($inventory|Where-Object InstanceId -ceq $id).Count -ne 1){throw "Selected device is absent or ambiguous: $id"}}
foreach($d in $inventory){[pscustomobject]@{InstanceId=$d.InstanceId;Name=$d.Name;Kind=$d.Kind;Status=if($InstanceId -contains $d.InstanceId){if($d.Status -eq 'OK'){'WouldDisable'}else{'AlreadyUnavailable'}}else{'NotSelected'};Risk=$d.Risk}}
}
function Invoke-WindSensorApply {
[CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)][string[]]$InstanceId,[Parameter(Mandatory)][string]$JournalPath)
$plan=@(Get-WindSensorPlan -InstanceId $InstanceId);if(@($plan|Where-Object Status -eq 'AlreadyUnavailable').Count){throw 'One or more selected devices are already unavailable; select only enabled devices.'}
if(-not $PSCmdlet.ShouldProcess('Selected PnP devices','Disable exact camera, microphone or sensor devices')){return $plan}
$j=[pscustomobject]@{Schema=1;Kind='WindSensors';CreatedUtc=[DateTime]::UtcNow.ToString('o');Status='Prepared';Entries=@($plan|Where-Object Status -eq 'WouldDisable'|ForEach-Object{[pscustomobject]@{InstanceId=$_.InstanceId;Name=$_.Name;Status='Pending'}})}
$save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 7}.GetNewClosure();&$save $j
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}
}
function Restore-WindSensors {
[CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)]$Journal,[Parameter(Mandatory)][scriptblock]$Save)
if($Journal.Schema-ne 1-or$Journal.Kind-ne'WindSensors'){throw 'Invalid sensor journal.'};if(-not$PSCmdlet.ShouldProcess('Selected PnP devices','Re-enable devices disabled by this journal')){return}
foreach($e in @($Journal.Entries)){if($e.Status -notin @('Disabled','Disabling')){continue};$d=Get-PnpDevice -InstanceId $e.InstanceId -ErrorAction Stop;if($d.Status -ne 'OK'){Enable-PnpDevice -InstanceId $e.InstanceId -Confirm:$false -ErrorAction Stop|Out-Null};$after=Get-PnpDevice -InstanceId $e.InstanceId -ErrorAction Stop;if($after.Status-ne'OK'){throw "Device restore readback failed: $($e.Name)"};$e.Status='Restored';&$Save $Journal};$Journal.Status='Restored';&$Save $Journal;$Journal
}
Export-ModuleMember -Function Get-WindSensorInventory,Get-WindSensorPlan,Invoke-WindSensorApply,Restore-WindSensors
+45
View File
@@ -0,0 +1,45 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Test-WindVeraCryptVolume {param([string]$Volume) return $Volume -match '^[A-Z]:$'}
function Resolve-WindVeraCryptPath {
param([string]$Path)
if($Path){$full=[IO.Path]::GetFullPath($Path);if(-not(Test-Path -LiteralPath $full -PathType Leaf)){throw 'Selected VeraCrypt executable does not exist.'};return $full}
$command=Get-Command VeraCrypt.exe -CommandType Application -ErrorAction SilentlyContinue;if($command){return $command.Source}
foreach($root in @($env:ProgramFiles,${env:ProgramFiles(x86)})){if($root){$candidate=Join-Path $root 'VeraCrypt\VeraCrypt.exe';if(Test-Path -LiteralPath $candidate -PathType Leaf){return $candidate}}}
return $null
}
function Invoke-WindLockEvent {
param([string[]]$VeraCryptVolume=@(),[string]$VeraCryptPath,[Parameter(Mandatory)][string]$ReportPath)
foreach($v in $VeraCryptVolume){if(-not(Test-WindVeraCryptVolume $v)){throw "Invalid VeraCrypt volume: $v"}}
$result=[pscustomobject]@{Schema=1;Kind='WindLockEvent';OccurredUtc=[DateTime]::UtcNow.ToString('o');Clipboard='Pending';Volumes=@();Status='Running'}
try{Add-Type -AssemblyName PresentationCore -ErrorAction Stop;[Windows.Clipboard]::Clear();$result.Clipboard='Cleared'}catch{$result.Clipboard='Failed: '+$_.Exception.Message}
foreach($v in $VeraCryptVolume){$entry=[pscustomobject]@{Volume=$v;Status='Pending'};$result.Volumes+=@($entry);try{if(-not$VeraCryptPath-or-not(Test-Path -LiteralPath $VeraCryptPath -PathType Leaf)){throw 'VeraCrypt executable unavailable.'};$p=Start-Process -FilePath $VeraCryptPath -ArgumentList @('/d',$v,'/q','/s') -Wait -PassThru -WindowStyle Hidden;if($p.ExitCode-ne 0){throw "VeraCrypt exit code $($p.ExitCode)"};$entry.Status='Unmounted'}catch{$entry.Status='Failed: '+$_.Exception.Message}}
$result.Status=if($result.Clipboard-eq'Cleared'-and-not@($result.Volumes|Where-Object Status -like 'Failed*').Count){'Completed'}else{'NeedsAttention'}
Write-MsPrivateJsonAtomic -InputObject $result -Path $ReportPath -Depth 6;$result
}
function Install-WindLockHandler {
param([string[]]$VeraCryptVolume=@(),[string]$VeraCryptPath,[Parameter(Mandatory)][string]$SourceRoot)
foreach($v in $VeraCryptVolume){if(-not(Test-WindVeraCryptVolume $v)){throw "Invalid VeraCrypt volume: $v"}}
if($VeraCryptVolume.Count){$VeraCryptPath=Resolve-WindVeraCryptPath $VeraCryptPath;if(-not$VeraCryptPath){throw 'VeraCrypt is not installed or VeraCrypt.exe was not supplied. No lock task was installed.'}}
$runtime=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\lock';[IO.Directory]::CreateDirectory($runtime)|Out-Null
foreach($rel in @('Lock-Event.ps1','src\Wind.Lock.psm1','src\MsPrivate.IO.psm1')){$dst=Join-Path $runtime $rel;[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($dst))|Out-Null;Copy-Item -LiteralPath (Join-Path $SourceRoot $rel) -Destination $dst -Force}
$cfg=[pscustomobject]@{Schema=1;VeraCryptVolume=@($VeraCryptVolume);VeraCryptPath=$VeraCryptPath};$cfgPath=Join-Path $runtime 'config.json';Write-MsPrivateJsonAtomic -InputObject $cfg -Path $cfgPath -Depth 4
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$script=Join-Path $runtime 'Lock-Event.ps1';$scriptXml=[Security.SecurityElement]::Escape($script);$cfgXml=[Security.SecurityElement]::Escape($cfgPath)
$xml=@"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo><Description>Clears the current user's clipboard and gracefully unmounts selected VeraCrypt volumes on session lock.</Description></RegistrationInfo>
<Triggers><SessionStateChangeTrigger><Enabled>true</Enabled><StateChange>SessionLock</StateChange><UserId>$sid</UserId></SessionStateChangeTrigger></Triggers>
<Principals><Principal id="Author"><UserId>$sid</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT2M</ExecutionTimeLimit></Settings>
<Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>-NoProfile -STA -ExecutionPolicy RemoteSigned -File &quot;$scriptXml&quot; -Config &quot;$cfgXml&quot;</Arguments></Exec></Actions>
</Task>
"@
Register-ScheduledTask -TaskName 'ms-private Session Lock' -Xml $xml -Force -ErrorAction Stop|Out-Null
}
function Remove-WindLockHandler {Unregister-ScheduledTask -TaskName 'ms-private Session Lock' -Confirm:$false -ErrorAction SilentlyContinue}
Export-ModuleMember -Function Test-WindVeraCryptVolume,Resolve-WindVeraCryptPath,Invoke-WindLockEvent,Install-WindLockHandler,Remove-WindLockHandler
+170
View File
@@ -0,0 +1,170 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-WindNetworkCatalog {
param([string]$Path=(Join-Path $PSScriptRoot '../config/network.json'))
$catalog=Get-Content -LiteralPath $Path -Raw|ConvertFrom-Json
if($catalog.Schema-ne 1-or[string]$catalog.RuleGroup-ne'ms-private'){throw 'Unsupported network catalog schema or rule group.'}
if(-not@($catalog.OptionalTelemetry).Count){throw 'Network catalog has no optional telemetry targets.'}
$ids=@{};$hosts=@{}
# These suffixes carry updating, trust/revocation, Defender/SmartScreen or WNS traffic.
# Optional telemetry catalogs must never target them.
$protectedSuffixes=@(
'windowsupdate.com','update.microsoft.com','delivery.mp.microsoft.com',
'definitionupdates.microsoft.com','wdcp.microsoft.com','smartscreen.microsoft.com',
'wns.windows.com','storequality.microsoft.com'
)
foreach($item in @($catalog.OptionalTelemetry)){
$id=[string]$item.Id;$fqdn=[string]$item.Fqdn
if($id-notmatch'^[a-z0-9][a-z0-9-]*$'-or$ids.ContainsKey($id)){throw "Invalid or duplicate network target ID: $id"}
if($fqdn-notmatch'^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$'-or$hosts.ContainsKey($fqdn.ToLowerInvariant())){throw "Invalid or duplicate network target FQDN: $fqdn"}
$normalized=$fqdn.ToLowerInvariant()
foreach($suffix in $protectedSuffixes){if($normalized-eq$suffix-or$normalized.EndsWith('.'+$suffix)){throw "Network target overlaps a protected security/update endpoint: $id"}}
$source=$null;if(-not[uri]::TryCreate([string]$item.Source,[UriKind]::Absolute,[ref]$source)-or$source.Scheme-ne'https'){throw "Network target requires an HTTPS source: $id"}
$ids[$id]=$true;$hosts[$fqdn.ToLowerInvariant()]=$true
}
$catalog
}
function Get-WindNetworkCapabilities {
$required=@('Get-NetFirewallProfile','Set-NetFirewallProfile','New-NetFirewallRule','Get-NetFirewallRule','Remove-NetFirewallRule')
$missing=@($required | Where-Object {-not (Get-Command $_ -ErrorAction SilentlyContinue)})
[pscustomobject]@{
FirewallAvailable=($missing.Count -eq 0)
DynamicFqdnAvailable=[bool](Get-Command New-NetFirewallDynamicKeywordAddress -ErrorAction SilentlyContinue)
ScheduledTaskAvailable=(-not @(@('New-ScheduledTaskAction','New-ScheduledTaskTrigger','New-ScheduledTaskPrincipal','Register-ScheduledTask','Unregister-ScheduledTask')|Where-Object{-not(Get-Command $_ -ErrorAction SilentlyContinue)}).Count)
PktmonAvailable=[bool](Get-Command pktmon.exe -ErrorAction SilentlyContinue)
Missing=@($missing)
}
}
function Get-WindNetworkPlan {
param([ValidateSet('Observe','BlockOptionalTelemetry','Isolate')][string]$Profile='Observe',[int]$RollbackMinutes=10)
if($RollbackMinutes -lt 2 -or $RollbackMinutes -gt 60){throw 'RollbackMinutes must be between 2 and 60.'}
$cap=Get-WindNetworkCapabilities
$items=[Collections.Generic.List[object]]::new()
if($Profile -eq 'Observe'){
$items.Add([pscustomobject]@{Id='observe';Status='Ready';Action='No firewall changes';Impact='Capture and inventory only.'})
} elseif($Profile -eq 'BlockOptionalTelemetry'){
$status=if($cap.FirewallAvailable -and $cap.DynamicFqdnAvailable){'WouldChange'}else{'Unsupported'}
foreach($target in @(Get-WindNetworkCatalog).OptionalTelemetry){
$items.Add([pscustomobject]@{Id=$target.Id;Status=$status;Action=('Block outbound FQDN '+$target.Fqdn);Impact=$target.Purpose;Source=$target.Source})
}
} else {
$status=if($cap.FirewallAvailable -and $cap.ScheduledTaskAvailable){'WouldChange'}else{'Unsupported'}
$items.Add([pscustomobject]@{Id='outbound-default';Status=$status;Action='Set default outbound action to Block on Domain, Private and Public profiles';Impact='Breaks most network activity until explicit allows exist. A local timed rollback is mandatory.'})
$items.Add([pscustomobject]@{Id='timed-rollback';Status=$status;Action="Restore prior firewall defaults after $RollbackMinutes minutes";Impact='Recovery runs locally as SYSTEM and does not require network access.'})
}
[pscustomobject]@{Schema=1;Profile=$Profile;Capabilities=$cap;RollbackMinutes=$RollbackMinutes;Items=@($items);Claim='Configuration preview only. No claim of zero telemetry or anonymity.'}
}
function New-WindNetworkBackend {
@{
ReadProfiles={ @(Get-NetFirewallProfile -PolicyStore ActiveStore | Select-Object Name,Enabled,DefaultInboundAction,DefaultOutboundAction) }
SetOutbound={param($name,$action) Set-NetFirewallProfile -Name $name -DefaultOutboundAction $action -ErrorAction Stop}
AddKeyword={param($id,$fqdn) New-NetFirewallDynamicKeywordAddress -Id $id -Keyword $fqdn -AutoResolve $true -ErrorAction Stop | Out-Null}
RemoveKeyword={param($id) $x=Get-NetFirewallDynamicKeywordAddress -Id $id -ErrorAction SilentlyContinue;if($x){$x|Remove-NetFirewallDynamicKeywordAddress -Confirm:$false -ErrorAction Stop}}
AddBlockRule={param($name,$id,$group) New-NetFirewallRule -Name $name -DisplayName $name -Group $group -Direction Outbound -Action Block -Enabled True -Profile Any -RemoteDynamicKeywordAddresses $id -ErrorAction Stop | Out-Null}
RemoveRules={param($group) Get-NetFirewallRule -Group $group -ErrorAction SilentlyContinue|Remove-NetFirewallRule -Confirm:$false -ErrorAction Stop}
ReadRules={param($group) @(Get-NetFirewallRule -Group $group -ErrorAction SilentlyContinue|Select-Object Name,DisplayName,Enabled,Direction,Action,Profile)}
ExportPolicy={param($path)& netsh.exe advfirewall export $path|Out-Null;if($LASTEXITCODE-ne 0){throw "Firewall export failed with exit code $LASTEXITCODE."}}
RegisterRollback={param($taskName,$script,$journal,$minutes)
$action=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ('-NoProfile -ExecutionPolicy Bypass -File "{0}" -Mode Restore -Journal "{1}" -FromRecoveryTask' -f $script,$journal)
$trigger=New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes($minutes)
$principal=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Force -ErrorAction Stop|Out-Null
}
RemoveRollback={param($taskName) Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue}
}
}
function Assert-WindNetworkJournal {
param([Parameter(Mandatory)]$Journal)
if($Journal.Schema-ne 1-or$Journal.Kind-ne'MsPrivateNetwork'-or$Journal.Profile-notin@('BlockOptionalTelemetry','Isolate')){throw 'Invalid network journal.'}
if($Journal.RuleGroup-ne'ms-private'){throw 'Unexpected network rule group.'}
if($Journal.RollbackTask -and [string]$Journal.RollbackTask -notmatch '^ms-private Network Recovery [0-9a-f]{32}$'){throw 'Invalid network recovery task name.'}
if($Journal.Profile-eq'Isolate'){
$profiles=@($Journal.Profiles);if($profiles.Count-ne 3-or@($profiles.Name|Sort-Object)-join','-ne'Domain,Private,Public'){throw 'Network journal must contain each firewall profile exactly once.'}
foreach($p in $profiles){if([string]$p.DefaultOutboundAction-notin@('Allow','Block','NotConfigured')){throw 'Invalid saved outbound action.'}}
} else {
foreach($item in @($Journal.KeywordIds)){$parsed=[guid]::Empty;if(-not[guid]::TryParse([string]$item.Id,[ref]$parsed)-or[string]$item.Rule-notmatch'^ms-private telemetry-'){throw 'Invalid dynamic keyword entry.'}}
}
}
function Invoke-WindNetworkApply {
[CmdletBinding(SupportsShouldProcess)]
param([ValidateSet('BlockOptionalTelemetry','Isolate')][string]$Profile,[int]$RollbackMinutes=10,[hashtable]$Backend=(New-WindNetworkBackend),[Parameter(Mandatory)][string]$JournalPath,[Parameter(Mandatory)][string]$RecoveryScript)
$plan=Get-WindNetworkPlan -Profile $Profile -RollbackMinutes $RollbackMinutes
if(@($plan.Items|Where-Object Status -ne 'WouldChange').Count){throw 'Required network capabilities are unavailable.'}
if(-not $PSCmdlet.ShouldProcess('Windows Firewall',"Apply $Profile network profile")){return $plan}
$catalog=Get-WindNetworkCatalog
if(@(& $Backend.ReadRules $catalog.RuleGroup).Count){throw 'ms-private firewall rules already exist. Restore their journal before applying another network profile.'}
$before=@(& $Backend.ReadProfiles)
$backup=$JournalPath+'.firewall.wfw'
if($Profile-eq'Isolate'){&$Backend.ExportPolicy $backup}
$journal=[pscustomobject]@{Schema=1;Kind='MsPrivateNetwork';CreatedUtc=[DateTime]::UtcNow.ToString('o');Profile=$Profile;Status='Prepared';RuleGroup=$catalog.RuleGroup;KeywordIds=@();Profiles=$before;RollbackTask=('ms-private Network Recovery '+[guid]::NewGuid().ToString('N'));BackupPath=if($Profile-eq'Isolate'){$backup}else{$null}}
$save={param($x) Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 8}.GetNewClosure()
& $save $journal
try{
if($Profile -eq 'BlockOptionalTelemetry'){
foreach($target in @($catalog.OptionalTelemetry)){
$id='{'+[guid]::NewGuid().ToString()+'}'; & $Backend.AddKeyword $id ([string]$target.Fqdn)
$journal.KeywordIds+=@([pscustomobject]@{Id=$id;Rule=('ms-private '+$target.Id)});& $save $journal
& $Backend.AddBlockRule ('ms-private '+$target.Id) $id $catalog.RuleGroup
}
} else {
& $Backend.RegisterRollback $journal.RollbackTask $RecoveryScript $JournalPath $RollbackMinutes
foreach($name in @('Domain','Private','Public')){& $Backend.SetOutbound $name 'Block'}
}
$journal.Status='Applied';& $save $journal
return $journal
}catch{
$journal.Status='ApplyFailed';& $save $journal
try{Restore-WindNetwork -Journal $journal -Backend $Backend -Save $save|Out-Null}catch{}
throw
}
}
function Restore-WindNetwork {
param([Parameter(Mandatory)]$Journal,[hashtable]$Backend=(New-WindNetworkBackend),[scriptblock]$Save)
Assert-WindNetworkJournal $Journal
if(-not $Save){$Save={param($x)}}
if($Journal.Profile -eq 'BlockOptionalTelemetry'){
& $Backend.RemoveRules ([string]$Journal.RuleGroup)
foreach($item in @($Journal.KeywordIds)){& $Backend.RemoveKeyword ([string]$item.Id)}
} elseif($Journal.Profile -eq 'Isolate'){
$current=@(& $Backend.ReadProfiles)
foreach($old in @($Journal.Profiles)){
$now=@($current|Where-Object Name -eq $old.Name)
if($now.Count -ne 1){throw "Firewall profile missing during restore: $($old.Name)"}
if([string]$now[0].DefaultOutboundAction -notin @('Block',[string]$old.DefaultOutboundAction)){throw "Firewall profile changed externally: $($old.Name)"}
& $Backend.SetOutbound ([string]$old.Name) ([string]$old.DefaultOutboundAction)
}
} else {throw 'Unknown network journal profile.'}
if($Journal.RollbackTask){& $Backend.RemoveRollback ([string]$Journal.RollbackTask)}
$Journal.Status='Restored';& $Save $Journal
$Journal
}
function Confirm-WindNetwork {
param([Parameter(Mandatory)]$Journal,[hashtable]$Backend=(New-WindNetworkBackend),[Parameter(Mandatory)][scriptblock]$Save)
Assert-WindNetworkJournal $Journal
if($Journal.Profile-ne'Isolate'-or$Journal.Status-ne'Applied'){throw 'Only an applied isolation journal can be confirmed.'}
& $Backend.RemoveRollback ([string]$Journal.RollbackTask);$Journal.Status='AppliedConfirmed';$Journal|Add-Member -NotePropertyName ConfirmedUtc -NotePropertyValue ([DateTime]::UtcNow.ToString('o')) -Force;&$Save $Journal;$Journal
}
function Get-WindNetworkValidation {
param([hashtable]$Backend=(New-WindNetworkBackend))
$catalog=Get-WindNetworkCatalog
[pscustomobject]@{
CapturedUtc=[DateTime]::UtcNow.ToString('o')
Profiles=@(& $Backend.ReadProfiles)
WindRules=@(& $Backend.ReadRules $catalog.RuleGroup)
TcpConnections=@(Get-NetTCPConnection -ErrorAction SilentlyContinue|Select-Object State,LocalAddress,LocalPort,RemoteAddress,RemotePort,OwningProcess)
UdpEndpoints=@(Get-NetUDPEndpoint -ErrorAction SilentlyContinue|Select-Object LocalAddress,LocalPort,OwningProcess)
Meaning='A point-in-time local observation. It does not prove that no traffic occurred.'
}
}
Export-ModuleMember -Function Get-WindNetworkCatalog,Get-WindNetworkCapabilities,Get-WindNetworkPlan,New-WindNetworkBackend,Invoke-WindNetworkApply,Restore-WindNetwork,Confirm-WindNetwork,Get-WindNetworkValidation,Assert-WindNetworkJournal
+61
View File
@@ -0,0 +1,61 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop
function Get-WindRecallState {
$context=Get-WindContext
$feature=$null
try{$feature=Get-WindowsOptionalFeature -Online -FeatureName Recall -ErrorAction Stop}catch{}
$path='SOFTWARE\Policies\Microsoft\Windows\WindowsAI';$name='DisableAIDataAnalysis'
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine','Registry64')
try{$key=$base.OpenSubKey($path);try{$exists=[bool]($key -and $key.GetValueNames() -contains $name);$value=if($exists){[int]$key.GetValue($name)}else{$null};$kind=if($exists){$key.GetValueKind($name).ToString()}else{$null}}finally{if($key){$key.Dispose()}}}finally{$base.Dispose()}
$eligibleEdition=$context.Edition -match '^(Professional|Enterprise|Education)'
$eligibleBuild=($context.Build -gt 26100 -or ($context.Build -eq 26100 -and $context.Revision -ge 3915))
[pscustomobject]@{Context=$context;FeaturePresent=[bool]$feature;FeatureState=if($feature){[string]$feature.State}else{'Unavailable'};PolicyExists=$exists;PolicyValue=$value;PolicyKind=$kind;PolicySupported=($eligibleEdition -and $eligibleBuild)}
}
function Get-WindRecallPlan {
param([switch]$RemoveFeature)
$state=Get-WindRecallState
$items=@([pscustomobject]@{Id='recall-policy';Status=if($state.PolicySupported){if($state.PolicyExists -and $state.PolicyValue -eq 1){'Configured'}elseif($state.PolicyExists -and $state.PolicyKind -ne 'DWord'){'Conflict'}else{'WouldChange'}}else{'Unsupported'};Action='Set DisableAIDataAnalysis=1';Impact='Disables Recall and deletes existing Recall snapshots. Snapshot deletion cannot be undone.'})
if($RemoveFeature){$items+= [pscustomobject]@{Id='recall-feature';Status=if(-not $state.FeaturePresent){'Unavailable'}elseif($state.FeatureState -eq 'Disabled'){'Configured'}else{'WouldChange'};Action='Disable optional feature Recall';Impact='May require a reboot. Restore can re-enable the feature, but cannot recover snapshots.'}}
[pscustomobject]@{Schema=1;State=$state;Items=$items;RequiresDeletionAcknowledgement=$true}
}
function Set-WindRecallPolicyState {
param($State)
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine','Registry64');$path='SOFTWARE\Policies\Microsoft\Windows\WindowsAI';$name='DisableAIDataAnalysis'
try{
if($State.Exists){$key=$base.CreateSubKey($path);try{$key.SetValue($name,[int]$State.Value,[Microsoft.Win32.RegistryValueKind]::DWord)}finally{$key.Dispose()}}
else{$key=$base.OpenSubKey($path,$true);if($key){try{$key.DeleteValue($name,$false)}finally{$key.Dispose()}}}
}finally{$base.Dispose()}
}
function Invoke-WindRecallApply {
[CmdletBinding(SupportsShouldProcess)]
param([switch]$RemoveFeature,[switch]$AcknowledgeSnapshotDeletion,[Parameter(Mandatory)][string]$JournalPath)
if(-not $AcknowledgeSnapshotDeletion){throw 'Recall changes require -AcknowledgeSnapshotDeletion because disabling the policy deletes existing snapshots.'}
$plan=Get-WindRecallPlan -RemoveFeature:$RemoveFeature
if(@($plan.Items|Where-Object Status -in @('Unsupported','Unavailable','Conflict')).Count){throw 'Recall plan contains unsupported, unavailable or conflicting actions.'}
if(-not $PSCmdlet.ShouldProcess('Windows Recall','Disable Recall and delete existing snapshots')){return $plan}
$s=$plan.State
$j=[pscustomobject]@{Schema=1;Kind='WindRecall';CreatedUtc=[DateTime]::UtcNow.ToString('o');Status='Prepared';Before=[pscustomobject]@{Policy=[pscustomobject]@{Exists=$s.PolicyExists;Kind=$s.PolicyKind;Value=$s.PolicyValue};FeatureState=$s.FeatureState};RemoveFeature=[bool]$RemoveFeature;Irreversible='Existing Recall snapshots deleted by policy cannot be restored.'}
$save={param($x)Write-MsPrivateJsonAtomic -InputObject $x -Path $JournalPath -Depth 7}.GetNewClosure();&$save $j
try{
Set-WindRecallPolicyState ([pscustomobject]@{Exists=$true;Value=1})
$after=Get-WindRecallState;if(-not $after.PolicyExists -or $after.PolicyValue -ne 1){throw 'Recall policy readback failed.'}
if($RemoveFeature -and $s.FeatureState -ne 'Disabled'){Disable-WindowsOptionalFeature -Online -FeatureName Recall -Remove -NoRestart -ErrorAction Stop|Out-Null}
$j.Status='Applied';&$save $j;$j
}catch{$j.Status='ApplyFailed';&$save $j;throw}
}
function Restore-WindRecall {
[CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)]$Journal,[Parameter(Mandatory)][scriptblock]$Save)
if($Journal.Schema -ne 1 -or $Journal.Kind -ne 'WindRecall'){throw 'Invalid Recall journal.'}
if(-not $PSCmdlet.ShouldProcess('Windows Recall','Restore saved Recall configuration')){return}
Set-WindRecallPolicyState $Journal.Before.Policy
if($Journal.RemoveFeature -and $Journal.Before.FeatureState -eq 'Enabled'){Enable-WindowsOptionalFeature -Online -FeatureName Recall -NoRestart -All -ErrorAction Stop|Out-Null}
$Journal.Status='RestoredConfiguration';$Journal.Note='Deleted snapshots cannot be recovered.';&$Save $Journal;$Journal
}
Export-ModuleMember -Function Get-WindRecallState,Get-WindRecallPlan,Invoke-WindRecallApply,Restore-WindRecall
+81
View File
@@ -0,0 +1,81 @@
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
function Get-WindRegistryDword {
param([string]$Path,[string]$Name)
try {
$item=Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction Stop
[pscustomobject]@{Readable=$true;Configured=$true;Value=[int]$item.$Name}
} catch [Management.Automation.ItemNotFoundException] {
[pscustomobject]@{Readable=$true;Configured=$false;Value=$null}
} catch [Management.Automation.PSArgumentException] {
[pscustomobject]@{Readable=$true;Configured=$false;Value=$null}
} catch {
[pscustomobject]@{Readable=$false;Configured=$null;Value=$null}
}
}
function Get-WindSecurityStatus {
$bitlocker=@();if(Get-Command Get-BitLockerVolume -ErrorAction SilentlyContinue){try{$bitlocker=@(Get-BitLockerVolume -ErrorAction Stop|Select-Object MountPoint,VolumeStatus,ProtectionStatus,EncryptionMethod,AutoUnlockEnabled)}catch{$bitlocker=@([pscustomobject]@{Status='Unreadable'})}}
$firewall=@();if(Get-Command Get-NetFirewallProfile -ErrorAction SilentlyContinue){try{$firewall=@(Get-NetFirewallProfile -PolicyStore ActiveStore|Select-Object Name,Enabled,DefaultInboundAction,DefaultOutboundAction)}catch{$firewall=@([pscustomobject]@{Status='Unreadable'})}}
$defender=$null
if(Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue){
try{$d=Get-MpComputerStatus -ErrorAction Stop;$defender=[pscustomobject]@{
AntivirusEnabled=$d.AntivirusEnabled;AntispywareEnabled=$d.AntispywareEnabled
RealTimeProtectionEnabled=$d.RealTimeProtectionEnabled;BehaviorMonitorEnabled=$d.BehaviorMonitorEnabled
IoavProtectionEnabled=$d.IoavProtectionEnabled;NISEnabled=$d.NISEnabled;OnAccessProtectionEnabled=$d.OnAccessProtectionEnabled
SignatureLastUpdated=$d.AntivirusSignatureLastUpdated;EngineVersion=$d.AMEngineVersion;PlatformVersion=$d.AMProductVersion
}}catch{$defender=[pscustomobject]@{Status='Unreadable'}}
}
$defenderPolicy=$null
if(Get-Command Get-MpPreference -ErrorAction SilentlyContinue){
try{$p=Get-MpPreference -ErrorAction Stop;$defenderPolicy=[pscustomobject]@{
CloudReporting=$p.MAPSReporting;SampleSubmission=$p.SubmitSamplesConsent;PUAProtection=$p.PUAProtection
NetworkProtection=$p.EnableNetworkProtection;ScriptScanningEnabled=(-not[bool]$p.DisableScriptScanning)
DownloadScanningEnabled=(-not[bool]$p.DisableIOAVProtection)
}}catch{$defenderPolicy=[pscustomobject]@{Status='Unreadable'}}
}
$secureBoot=$null;try{$secureBoot=[bool](Confirm-SecureBootUEFI -ErrorAction Stop)}catch{$secureBoot='Unavailable or unreadable'}
$tpm=$null;if(Get-Command Get-Tpm -ErrorAction SilentlyContinue){try{$t=Get-Tpm -ErrorAction Stop;$tpm=[pscustomobject]@{Present=$t.TpmPresent;Ready=$t.TpmReady;Enabled=$t.TpmEnabled;Activated=$t.TpmActivated;Owned=$t.TpmOwned;RestartPending=$t.RestartPending}}catch{$tpm=[pscustomobject]@{Status='Unreadable'}}}
$deviceGuard=$null
try{
$dg=Get-CimInstance -Namespace 'root\Microsoft\Windows\DeviceGuard' -ClassName Win32_DeviceGuard -ErrorAction Stop
$deviceGuard=[pscustomobject]@{
VirtualizationBasedSecurityStatus=$dg.VirtualizationBasedSecurityStatus
SecurityServicesConfigured=@($dg.SecurityServicesConfigured)
SecurityServicesRunning=@($dg.SecurityServicesRunning)
AvailableSecurityProperties=@($dg.AvailableSecurityProperties)
}
}catch{$deviceGuard=[pscustomobject]@{Status='Unavailable or unreadable'}}
$lsa=[pscustomobject]@{
RunAsPPL=Get-WindRegistryDword 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'RunAsPPL'
RunAsPPLBoot=Get-WindRegistryDword 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'RunAsPPLBoot'
}
$uac=[pscustomobject]@{
EnableLUA=Get-WindRegistryDword 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' 'EnableLUA'
ConsentPromptBehaviorAdmin=Get-WindRegistryDword 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' 'ConsentPromptBehaviorAdmin'
}
$remoteAccess=[pscustomobject]@{
RemoteDesktopDenied=Get-WindRegistryDword 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections'
RemoteAssistance=Get-WindRegistryDword 'HKLM:\SYSTEM\CurrentControlSet\Control\Remote Assistance' 'fAllowToGetHelp'
}
$smbServer=$null;if(Get-Command Get-SmbServerConfiguration -ErrorAction SilentlyContinue){try{$s=Get-SmbServerConfiguration -ErrorAction Stop;$smbServer=$s|Select-Object EnableSMB1Protocol,EnableSMB2Protocol,RequireSecuritySignature,EncryptData,EnableInsecureGuestLogons}catch{$smbServer=[pscustomobject]@{Status='Unreadable'}}}
$smbClient=$null;if(Get-Command Get-SmbClientConfiguration -ErrorAction SilentlyContinue){try{$s=Get-SmbClientConfiguration -ErrorAction Stop;$smbClient=$s|Select-Object RequireSecuritySignature,EnableInsecureGuestLogons}catch{$smbClient=[pscustomobject]@{Status='Unreadable'}}}
$smb1Feature=$null;if(Get-Command Get-WindowsOptionalFeature -ErrorAction SilentlyContinue){try{$smb1Feature=(Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop|Select-Object FeatureName,State)}catch{$smb1Feature=[pscustomobject]@{Status='Unreadable'}}}
$services=@();if(Get-Command Get-Service -ErrorAction SilentlyContinue){foreach($name in @('wuauserv','BITS','CryptSvc','WinDefend','WdNisSvc','mpssvc','EventLog')){try{$s=Get-Service -Name $name -ErrorAction Stop;$services+=@([pscustomobject]@{Name=$name;Status=[string]$s.Status;StartType=[string]$s.StartType})}catch{$services+=@([pscustomobject]@{Name=$name;Status='Unavailable'})}}}
[pscustomobject]@{
Schema=2;CapturedUtc=[DateTime]::UtcNow.ToString('o');BitLocker=$bitlocker;Firewall=$firewall
Defender=$defender;DefenderPolicy=$defenderPolicy;SecureBoot=$secureBoot;Tpm=$tpm;DeviceGuard=$deviceGuard
LsaProtection=$lsa;Uac=$uac;RemoteAccess=$remoteAccess;SmbServer=$smbServer;SmbClient=$smbClient;Smb1Feature=$smb1Feature
ProtectedServices=$services;RecoveryKeysCaptured=$false;SensitiveValuesCaptured=$false
Meaning='Read-only local security posture. Status fields require interpretation for the current Windows edition and build; no secret, recovery key, account name or Defender exclusion is collected.'
}
}
Export-ModuleMember -Function Get-WindSecurityStatus
+242 -117
View File
@@ -1,136 +1,261 @@
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
function ConvertFrom-WindChoice { function New-WindMenuItem {
param([string]$Text,[int]$Count) param([string]$Id,[string]$Label,[string]$Detail='',[bool]$Disabled=$false)
if ([string]::IsNullOrWhiteSpace($Text) -or $Text.Trim() -eq 'none') { return } [pscustomobject]@{Id=$Id;Label=$Label;Detail=$Detail;Disabled=$Disabled}
if ($Text.Trim() -eq 'all') { if ($Count -gt 0) { 0..($Count-1) }; return } }
$result = @() function Get-WindWrappedText {
foreach ($token in ($Text -split '[,\s]+' | Where-Object { $_ })) { param([string]$Text,[int]$Width)
$number = 0 $remaining=$Text -replace '[\x00-\x1f\x7f-\x9f]',' '
if (-not [int]::TryParse($token,[ref]$number) -or $number -lt 1 -or $number -gt $Count) { throw "Enter numbers from 1 to $Count, separated by commas, or all/none." } $Width=[Math]::Max(1,$Width)
$result += $number - 1 while ($remaining.Length -gt $Width) {
} $cut=$remaining.LastIndexOf(' ',$Width-1,$Width)
$result | Select-Object -Unique if ($cut -lt [Math]::Floor($Width/2)) {$cut=$Width}
$remaining.Substring(0,$cut).TrimEnd(); $remaining=$remaining.Substring($cut).TrimStart()
}
$remaining
}
function New-WindMenuState {
param([string[]]$Selected=@())
$checked=@{}; foreach ($id in $Selected) {$checked[$id]=$true}
[pscustomobject]@{Cursor=0;Filter='';Checked=$checked;Action='';PageSize=10}
}
function Get-WindVisibleItems {
param([object[]]$Items,$State)
foreach ($item in $Items) {
if (-not $State.Filter -or $item.Label.IndexOf($State.Filter,[StringComparison]::OrdinalIgnoreCase) -ge 0) {$item}
}
}
function Update-WindMenuState {
param($State,[object[]]$Items,[string]$Key,[char]$Character=[char]0,[switch]$Multiple,[switch]$Searchable)
$visible=@(Get-WindVisibleItems $Items $State); $count=$visible.Count; $State.Action=''
switch ($Key) {
'UpArrow' {if ($count) {$State.Cursor=($State.Cursor+$count-1)%$count}}
'DownArrow' {if ($count) {$State.Cursor=($State.Cursor+1)%$count}}
'Home' {$State.Cursor=0}
'End' {$State.Cursor=[Math]::Max(0,$count-1)}
'PageUp' {$State.Cursor=[Math]::Max(0,$State.Cursor-$State.PageSize)}
'PageDown' {$State.Cursor=[Math]::Max(0,[Math]::Min($count-1,$State.Cursor+$State.PageSize))}
'Escape' {$State.Action='Cancel'}
'Enter' {if ($Multiple -or ($count -and -not $visible[$State.Cursor].Disabled)) {$State.Action='Accept'}}
'Spacebar' {
if ($Multiple -and $count -and -not $visible[$State.Cursor].Disabled) {
$id=$visible[$State.Cursor].Id
if ($State.Checked.ContainsKey($id)) {$State.Checked.Remove($id)} else {$State.Checked[$id]=$true}
}
}
'Backspace' {if ($Searchable -and $State.Filter.Length) {$State.Filter=$State.Filter.Substring(0,$State.Filter.Length-1);$State.Cursor=0}}
default {if ($Searchable -and -not [char]::IsControl($Character) -and $Character -ne [char]0) {$State.Filter+=$Character;$State.Cursor=0}}
}
}
function Get-WindMenuFrame {
param([string]$Title,[string]$Subtitle,[object[]]$Items,$State,[int]$Width=90,[int]$Height=28,[switch]$Multiple,[switch]$Searchable)
$Width=[Math]::Max(30,$Width);$Height=[Math]::Max(16,$Height)
$visible=@(Get-WindVisibleItems $Items $State)
$State.PageSize=[Math]::Max(3,$Height-13)
$State.Cursor=[Math]::Max(0,[Math]::Min($State.Cursor,$visible.Count-1))
$frame=[Collections.Generic.List[object]]::new()
$frame.Add([pscustomobject]@{Text=' m s - p r i v a t e';Color='Cyan';Active=$false})
$frame.Add([pscustomobject]@{Text=(' '+$Title);Color='White';Active=$false})
$frame.Add([pscustomobject]@{Text=(' '+$Subtitle);Color='DarkGray';Active=$false})
$frame.Add([pscustomobject]@{Text=(' '+('-'*($Width-4)));Color='DarkGray';Active=$false})
$top=[int][Math]::Floor($State.Cursor/$State.PageSize)*$State.PageSize
for ($i=$top;$i -lt [Math]::Min($top+$State.PageSize,$visible.Count);$i++) {
$item=$visible[$i];$active=$i -eq $State.Cursor
$prefix=if ($active) {' > '} else {' '}
if ($Multiple) {$prefix+=if ($State.Checked.ContainsKey($item.Id)) {'[x] '} else {'[ ] '}}
if ($item.Disabled) {$prefix+='- '}
$frame.Add([pscustomobject]@{Text=$prefix+($item.Label -replace '[\x00-\x1f\x7f-\x9f]',' ');Color=$(if ($item.Disabled) {'DarkGray'} elseif ($active) {'White'} else {'Gray'});Active=$active})
}
if (-not $visible.Count) {$frame.Add([pscustomobject]@{Text=' No matches. Backspace clears the filter.';Color='Yellow';Active=$false})}
while ($frame.Count -lt 4+$State.PageSize) {$frame.Add([pscustomobject]@{Text='';Color='Gray';Active=$false})}
$frame.Add([pscustomobject]@{Text=(' '+('-'*($Width-4)));Color='DarkGray';Active=$false})
$detail=if ($visible.Count) {$visible[$State.Cursor].Detail} else {''}
$detailLines=@(Get-WindWrappedText $detail ($Width-4))
for ($i=0;$i -lt 3;$i++) {$frame.Add([pscustomobject]@{Text=$(if ($i -lt $detailLines.Count) {' '+$detailLines[$i]} else {''});Color='Gray';Active=$false})}
$position=if ($visible.Count) {'{0}/{1}' -f ($State.Cursor+1),$visible.Count} else {'0/0'}
$info=if ($Searchable) {"Filter: $($State.Filter)"} else {''}
if ($Multiple) {$info="Selected: $($State.Checked.Count) $info"}
$frame.Add([pscustomobject]@{Text=" $position $info";Color='DarkGray';Active=$false})
$hint=if ($Multiple) {'Arrows move Space toggle Enter save Esc cancel'} else {'Arrows move Enter select Esc back'}
$frame.Add([pscustomobject]@{Text=(' '+$hint);Color='Cyan';Active=$false})
if ($Searchable) {$frame.Add([pscustomobject]@{Text=' Type to filter Backspace erase PgUp/PgDn scroll';Color='DarkGray';Active=$false})}
foreach ($line in $frame) {if ($line.Text.Length -gt $Width-1) {$line.Text=$line.Text.Substring(0,$Width-4)+'...'};$line}
}
function Select-WindMenu {
param([string]$Title,[string]$Subtitle='',[object[]]$Items,[string[]]$Selected=@(),[switch]$Multiple,[switch]$Searchable)
$state=New-WindMenuState $Selected
if (-not $Multiple -and $Selected.Count) {
for ($i=0;$i -lt $Items.Count;$i++) {if ($Items[$i].Id -eq $Selected[0]) {$state.Cursor=$i;break}}
}
$cursorVisible=[Console]::CursorVisible
try {
[Console]::CursorVisible=$false;Clear-Host;$lastSize=''
while ($true) {
$width=[Console]::WindowWidth;$height=[Console]::WindowHeight
if ($width -lt 40 -or $height -lt 18) {throw 'Enlarge the terminal to at least 40 columns and 18 rows.'}
if ($lastSize -ne "$width/$height") {Clear-Host;$lastSize="$width/$height"}
$frame=@(Get-WindMenuFrame -Title $Title -Subtitle $Subtitle -Items $Items -State $state -Width $width -Height $height -Multiple:$Multiple -Searchable:$Searchable)
[Console]::SetCursorPosition(0,0)
foreach ($line in $frame) {Write-Host $line.Text.PadRight($width-1) -ForegroundColor $line.Color -BackgroundColor $(if ($line.Active) {'DarkBlue'} else {'Black'})}
$key=[Console]::ReadKey($true)
if ($key.Key -eq 'C' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) {$state.Action='Cancel'}
else {Update-WindMenuState -State $state -Items $Items -Key $key.Key.ToString() -Character $key.KeyChar -Multiple:$Multiple -Searchable:$Searchable}
if ($state.Action -eq 'Cancel') {return [pscustomobject]@{Accepted=$false;Ids=@()}}
if ($state.Action -eq 'Accept') {
$ids=if ($Multiple) {@($Items | Where-Object {$state.Checked.ContainsKey($_.Id)} | ForEach-Object {$_.Id})} else {@(@(Get-WindVisibleItems $Items $state)[$state.Cursor].Id)}
return [pscustomobject]@{Accepted=$true;Ids=@($ids)}
}
}
} finally {[Console]::CursorVisible=$cursorVisible;[Console]::ResetColor();Clear-Host}
}
function Show-WindText {
param([string]$Title,[string[]]$Lines)
$items=@(New-WindMenuItem 'back' 'Back' 'Esc or Enter returns to the previous screen.');$number=0
foreach ($line in $Lines) {
foreach ($part in @(Get-WindWrappedText $line ([Math]::Max(30,[Console]::WindowWidth-8)))) {$number++;$items+=New-WindMenuItem "line-$number" $part $line $true}
}
Select-WindMenu -Title $Title -Subtitle 'Arrows and Page Up / Page Down scroll' -Items $items | Out-Null
}
function Get-WindSetupSnapshot {
param($Settings)
$inventory=@(Get-WindAppInventory);$ids=@($inventory | ForEach-Object {$_.Id})
$Settings.AppOptions.ExtraId=@($Settings.AppOptions.ExtraId | Where-Object {$ids -contains $_})
$Settings.AppOptions.ExcludeId=@($Settings.AppOptions.ExcludeId | Where-Object {$ids -contains $_})
$options=$Settings.AppOptions
$sensors=@();if($Settings.DisableSensors){$sensors=@(Get-WindSensorInventory|Where-Object Status -eq 'OK')}
$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}
}
function Show-WindSetupReview {
param($Snapshot,$Settings)
$items=@(New-WindMenuItem 'back' 'Back to setup' 'Nothing changes on this screen.')
foreach ($p in $Snapshot.Policies) {$items+=New-WindMenuItem $p.Id ("[$($p.Status)] $($p.Title)") $p.Impact $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}
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
}
function Set-WindSetupPreset {
param($Settings,$Preset)
$Settings.Preset=$Preset.Id;$Settings.Profile=$Preset.Profile;$Settings.Updates=$Preset.Updates;$Settings.Browsers=@($Preset.Browsers);$Settings.AccountPurge=$Preset.Accounts
$Settings.Services=$Preset.Services;$Settings.Network=$Preset.Network;$Settings.RollbackMinutes=[int]$Preset.RollbackMinutes;$Settings.Recall=$Preset.Recall;$Settings.RemoveRecall=$Preset.RemoveRecall
$Settings.Cleanup=$Preset.Cleanup;$Settings.PrivacyCleanup=$Preset.PrivacyCleanup;$Settings.CleanupSchedule=$Preset.CleanupSchedule;$Settings.Lock=$Preset.Lock;$Settings.InstallMullvad=$Preset.InstallMullvad;$Settings.InstallVeraCrypt=$Preset.InstallVeraCrypt;$Settings.DisableSensors=$Preset.DisableSensors
$Settings.AppOptions=@{ExtraId=@();ExcludeId=@();SkipBundled=(-not$Preset.RemoveBundled);KeepOneDrive=(-not$Preset.RemoveOneDrive);KeepEdge=(-not$Preset.RemoveEdge);RemovePersonal=[bool]$Preset.RemovePersonal;RemoveStore=[bool]$Preset.RemoveStore}
} }
function Start-WindMenu { function Start-WindMenu {
param([Parameter(Mandatory)][string]$Root,[string]$Profile='Standard',[string]$Updates='Keep') param([Parameter(Mandatory)][string]$Root,[string]$Profile='Standard',[string]$Updates='Keep')
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or [Console]::IsInputRedirected) {throw 'Open ms-private in Windows Terminal or a PowerShell console. For automation, use the explicit CLI modes.'}
if ([Console]::IsInputRedirected) { throw 'The menu needs an interactive terminal. Use -Mode Audit/Plan or Manage-Apps.ps1 for scripts.' } Import-Module (Join-Path $Root 'src/Wind.Core.psm1')
Import-Module (Join-Path $Root 'src/Wind.Core.psm1') -Force Import-Module (Join-Path $Root 'src/Wind.Apps.psm1')
Import-Module (Join-Path $Root 'src/Wind.Apps.psm1') -Force Import-Module (Join-Path $Root 'src/Wind.Hardware.psm1')
$browsers = @('Zen') Import-Module (Join-Path $Root 'src/Wind.Recall.psm1')
$appOptions = @{ ExtraId=@(); ExcludeId=@(); SkipBundled=$false; KeepOneDrive=$false; KeepEdge=$false } Import-Module (Join-Path $Root 'src/MsPrivate.Presets.psm1')
Import-Module (Join-Path $Root 'src/MsPrivate.Execution.psm1')
$installContext=Get-MsPrivateInstallContext
$settings=[pscustomobject]@{Preset='Custom';Profile=$Profile;Updates=$Updates;Browsers=@('Zen');AccountPurge=($Profile-eq'Strict');Services=($Profile-eq'Strict');Network='Observe';RollbackMinutes=10;Recall=$false;RemoveRecall=$false;Cleanup=$false;PrivacyCleanup=$false;CleanupSchedule=$false;Lock=$false;InstallMullvad=$false;InstallVeraCrypt=$false;DisableSensors=$false;AppOptions=@{ExtraId=@();ExcludeId=@();SkipBundled=$false;KeepOneDrive=$false;KeepEdge=$false;RemovePersonal=$false;RemoveStore=$false}}
Set-WindSetupPreset $settings (Get-MsPrivatePreset $installContext.RecommendedPreset)
$focus='review'
while ($true) { while ($true) {
Write-Host '' $options=$settings.AppOptions
Write-Host 'WIND / privacy setup' -ForegroundColor Cyan $items=@(
Write-Host ("Profile: {0} Updates: {1} Browsers: {2}" -f $Profile,$Updates,($browsers -join ', ')) (New-WindMenuItem 'review' 'Review plan' 'See which policies and applications are selected before applying.'),
Write-Host ('Bundled apps: {0} OneDrive: {1} Edge: {2}' -f $(if ($appOptions.SkipBundled) {'keep'} else {'remove'}),$(if ($appOptions.KeepOneDrive) {'keep'} else {'remove'}),$(if ($appOptions.KeepEdge) {'keep'} else {'remove where supported'})) (New-WindMenuItem 'preset' ('Preset '+$settings.Preset) ("Auto recommendation: $($installContext.RecommendedPreset). $($installContext.Reason) Fresh Install performs the complete workflow; Established Safe keeps destructive groups off.")),
Write-Host '1 Review plan 2 Privacy profile 3 Browsers 4 Apps' (New-WindMenuItem 'profile' ('Privacy profile '+$settings.Profile) 'Strict adds app-permission denials, activity-history controls and reversible shutdown of reviewed diagnostic, connected-device, cloud-sync, location and error-reporting services.'),
Write-Host '5 Update mode 6 Apply plan 7 Audit 8 Restore policies' (New-WindMenuItem 'browsers' ('Browsers '+($settings.Browsers -join ', ')) 'Choose one or several browsers. Installation is verified before apps are removed.'),
Write-Host '9 Default browser settings G Home guide Q Quit' (New-WindMenuItem 'apps' 'Apps & debloat' 'Medium and above select every unambiguously removable Microsoft and third-party app. Drivers, runtimes, security tools, browsers and recovery components remain protected.'),
$choice = (Read-Host 'Choose').Trim().ToUpperInvariant() (New-WindMenuItem 'accounts' ('Microsoft app accounts '+$(if($settings.AccountPurge){'Purge'}else{'Keep'})) 'Strict can reset Store, Xbox, packaged Office/Teams and the shared account broker, then delete matching stored Microsoft application credentials. Windows logon remains.'),
if ($choice -eq 'Q') { return } (New-WindMenuItem 'updates' ('Windows updates '+$settings.Updates) 'Keep the update engine intact. Manual mode makes you responsible for security updates.'),
(New-WindMenuItem 'apply' 'Apply selected changes' 'Review, then confirm once. App removals cannot be undone by policy restore.'),
(New-WindMenuItem 'restore' 'Restore policies' 'Restore saved policy values. Reinstall removed apps from their official sources.'),
(New-WindMenuItem 'defaults' 'Default browser settings' 'Open Windows Settings to choose which browser opens links.'),
(New-WindMenuItem 'guide' 'Privacy guide' 'Manual privacy settings for Home editions and unsupported builds.'),
(New-WindMenuItem 'gui' 'Open graphical wizard' 'Configure network, Recall, exact hardware devices, cleanup and lock handling in a tabbed Windows interface.'),
(New-WindMenuItem 'quit' 'Quit' 'Close ms-private. Unapplied choices are not saved.'))
$choice=Select-WindMenu -Title 'Make Windows yours.' -Subtitle 'Privacy controls / app cleanup / browser choice' -Items $items -Selected @($focus)
if (-not $choice.Accepted -or $choice.Ids[0] -eq 'quit') {return}
$focus=$choice.Ids[0]
try { try {
switch ($choice) { switch ($choice.Ids[0]) {
'1' { 'review' {Show-WindSetupReview (Get-WindSetupSnapshot $settings) $settings}
& (Join-Path $Root 'Wind.ps1') -Mode Plan -Profile $Profile -Updates $Updates 'preset' {
& (Join-Path $Root 'Manage-Apps.ps1') -Mode Plan -Browser $browsers @appOptions $catalog=Get-MsPrivatePresetCatalog
$r=Select-WindMenu -Title 'Choose an automatic preset' -Subtitle 'Review shows every exact app and device before Apply' -Items @($catalog.Presets|ForEach-Object{New-WindMenuItem $_.Id $_.Label $_.Description}) -Selected @($settings.Preset)
if($r.Accepted){Set-WindSetupPreset $settings (Get-MsPrivatePreset $r.Ids[0])}
} }
'2' { 'profile' {
Write-Host '1 Standard: privacy controls; no Delivery Optimization peering' $r=Select-WindMenu -Title 'Privacy profile' -Items @((New-WindMenuItem 'Standard' 'Standard' 'Core privacy controls with clipboard history permitted and Windows security/update services preserved.'),(New-WindMenuItem 'Strict' 'Strict' 'Adds app-permission denials, activity-history controls, clipboard-history disablement and the reviewed privacy-service shutdown. Returning to Standard does not recover cleared history.'))
Write-Host '2 Strict: also disable clipboard history and the DO cloud service' if ($r.Accepted) {$settings.Preset='Custom';$settings.Profile=$r.Ids[0];$settings.AccountPurge=($settings.Profile-eq'Strict');$settings.Services=($settings.Profile-eq'Strict')}
Write-Host 'Switching back to Standard permits clipboard history; it cannot restore deleted history.'
switch (Read-Host 'Profile [1/2]') { '1' {$Profile='Standard'} '2' {$Profile='Strict'} default {throw 'Choose 1 or 2.'} }
} }
'3' { 'accounts' {
$names = @('Zen','Helium','Tor') $r=Select-WindMenu -Title 'Microsoft application accounts' -Items @((New-WindMenuItem 'keep' 'Keep current app sign-ins' 'Future authentication is still governed by the chosen policy profile.'),(New-WindMenuItem 'purge' 'Purge current app sign-ins' 'Permanently resets current-user Store, Xbox, packaged Office/Teams and shared account-broker data, and deletes matching stored Microsoft application credentials.')) -Selected @($(if($settings.AccountPurge){'purge'}else{'keep'}))
Write-Host '1 Zen 2 Helium 3 Tor Browser' if($r.Accepted){$settings.Preset='Custom';$settings.AccountPurge=($r.Ids[0]-eq'purge')}
Write-Host 'Select any combination. Keep Tor settings separate from personal browsing.'
$indices = @(ConvertFrom-WindChoice -Text (Read-Host 'Numbers, all, or none') -Count 3)
$browsers = @($indices | ForEach-Object { $names[$_] })
if (-not $browsers.Count) { $appOptions.KeepEdge=$true; Write-Host 'Edge kept because no replacement browser is selected.' }
} }
'4' { 'browsers' {
Write-Host '1 Select individual apps 2 Toggle bundled removal 3 Toggle OneDrive 4 Toggle Edge' $r=Select-WindMenu -Title 'Choose your browsers' -Subtitle 'Select any combination' -Items @((New-WindMenuItem 'Zen' 'Zen' 'Firefox-based browser for everyday browsing.'),(New-WindMenuItem 'Helium' 'Helium' 'Chromium-based browser for everyday browsing.'),(New-WindMenuItem 'Tor' 'Tor Browser' 'Keep stock privacy protections. Personal account sign-ins still identify you.')) -Selected $settings.Browsers -Multiple
switch (Read-Host 'App option') { if ($r.Accepted) {$settings.Preset='Custom';$settings.Browsers=@($r.Ids);if (-not $r.Ids.Count) {$options.KeepEdge=$true}}
'1' { }
$inventory = @(Get-WindAppInventory) 'apps' {
# Remove stale selections after a completed or partially completed operation. $selected=@();if (-not $options.SkipBundled) {$selected+='bundled'};if (-not $options.KeepOneDrive) {$selected+='onedrive'};if (-not $options.KeepEdge) {$selected+='edge'}
$appOptions.ExtraId = @($appOptions.ExtraId | Where-Object { $inventory.Id -contains $_ }) if($options.RemovePersonal){$selected+='personal'};if($options.RemoveStore){$selected+='store'}
$appOptions.ExcludeId = @($appOptions.ExcludeId | Where-Object { $inventory.Id -contains $_ }) $r=Select-WindMenu -Title 'Default app removals' -Subtitle 'Space toggles groups; Enter continues to individual apps' -Items @((New-WindMenuItem 'bundled' 'Bundled / promotional apps' 'Exact curated catalog names.'),(New-WindMenuItem 'personal' 'All removable personal / third-party apps' 'Selects every discovered app except protected dependencies, security tools and chosen privacy applications.'),(New-WindMenuItem 'store' 'Microsoft Store and purchase app' 'Current-user removal. Reinstallation may require an official recovery command or Windows repair.'),(New-WindMenuItem 'onedrive' 'OneDrive' 'Stops sync. Cloud-only files stay online. Download and back up needed files before removal.'),(New-WindMenuItem 'edge' 'Microsoft Edge' 'Supported uninstaller only, after replacement browsers are installed. Windows may refuse removal. WebView2 stays installed.')) -Selected $selected -Multiple
$selectable = @(Get-WindAppPlan -Inventory $inventory @appOptions | Where-Object { -not $_.Protected } | Sort-Object Name) if (-not $r.Accepted) {break}
for ($i=0; $i -lt $selectable.Count; $i++) { $settings.Preset='Custom';$options.RemovePersonal=($r.Ids-contains'personal');$options.RemoveStore=($r.Ids-contains'store')
Write-Host ('{0,3} [{1}] {2} ({3})' -f ($i+1),$(if ($selectable[$i].Status -eq 'Remove') {'x'} else {' '}),$selectable[$i].Name,$selectable[$i].Scope) $beforeGroups=Get-WindSetupSnapshot $settings
} Set-WindRemovalGroups -Options $options -Inventory $beforeGroups.Apps -Bundled ($r.Ids -contains 'bundled') -OneDrive ($r.Ids -contains 'onedrive') -Edge ($r.Ids -contains 'edge')
Write-Host 'Numbers toggle the displayed selections. Blank returns. Protected components are excluded.' $snapshot=Get-WindSetupSnapshot $settings
$indices = @(ConvertFrom-WindChoice -Text (Read-Host 'Toggle numbers') -Count $selectable.Count) $apps=@($snapshot.Apps | Where-Object {-not $_.Protected} | Sort-Object Name)
foreach ($i in $indices) { $rows=@($apps | ForEach-Object {New-WindMenuItem $_.Id $_.Name ("$($_.Scope) / $($_.Version). $($_.Reason). Uninstalling can remove app data.")})
$app = $selectable[$i] $r=Select-WindMenu -Title 'Applications to remove' -Subtitle 'Checked = remove. Personal apps start unchecked. Dependencies are hidden.' -Items $rows -Selected @($apps | Where-Object Status -eq 'Remove' | ForEach-Object {$_.Id}) -Multiple -Searchable
if ($app.Status -eq 'Remove') { if ($r.Accepted) {
$appOptions.ExtraId = @($appOptions.ExtraId | Where-Object { $_ -ne $app.Id }) $options.ExtraId=@($r.Ids);$options.ExcludeId=@($apps | Where-Object {$r.Ids -notcontains $_.Id} | ForEach-Object {$_.Id})
$appOptions.ExcludeId = @($appOptions.ExcludeId + $app.Id | Select-Object -Unique) if (@($apps | Where-Object {$_.Name -eq 'Microsoft Edge' -and $r.Ids -contains $_.Id}).Count) {$options.KeepEdge=$false}
} else { if (@($apps | Where-Object {$_.Name -eq 'Microsoft OneDrive' -and $r.Ids -contains $_.Id}).Count) {$options.KeepOneDrive=$false}
if ($app.Name -eq 'Microsoft Edge') { $appOptions.KeepEdge=$false }
if ($app.Name -eq 'Microsoft OneDrive') { $appOptions.KeepOneDrive=$false }
$appOptions.ExcludeId = @($appOptions.ExcludeId | Where-Object { $_ -ne $app.Id })
$appOptions.ExtraId = @($appOptions.ExtraId + $app.Id | Select-Object -Unique)
}
}
}
'2' {$appOptions.SkipBundled = -not $appOptions.SkipBundled}
'3' {$appOptions.KeepOneDrive = -not $appOptions.KeepOneDrive}
'4' {$appOptions.KeepEdge = -not $appOptions.KeepEdge}
default {throw 'Choose 1, 2, 3 or 4.'}
} }
} }
'5' { 'updates' {
Write-Host '1 Keep current settings 2 Automatic 3 Notify before download 4 Manual' $r=Select-WindMenu -Title 'Windows updates' -Items @((New-WindMenuItem 'Keep' 'Keep current settings' 'Leave update policy unchanged, including any previous ms-private setting.'),(New-WindMenuItem 'Automatic' 'Automatic' 'Request automatic updates using the existing Windows Update engine.'),(New-WindMenuItem 'Notify' 'Notify before downloading' 'Request notification before downloads; supported policy editions only.'),(New-WindMenuItem 'Manual' 'Manual' 'Disable automatic updates. You must install security updates yourself. This does not block update traffic.'))
Write-Host 'Manual turns off automatic updates. You become responsible for installing security updates.' if ($r.Accepted) {$settings.Preset='Custom';$settings.Updates=$r.Ids[0]}
$names=@('Keep','Automatic','Notify','Manual')
$indices=@(ConvertFrom-WindChoice -Text (Read-Host 'Update mode [1-4]') -Count 4)
if ($indices.Count -ne 1) { throw 'Choose one update mode.' }
$Updates=$names[$indices[0]]
} }
'6' { 'apply' {
Assert-WindMutationAllowed Assert-WindMutationAllowed;$snapshot=Get-WindSetupSnapshot $settings;Show-WindSetupReview $snapshot $settings
$inventory = @(Get-WindAppInventory) $recallDisposition=Resolve-MsPrivateRecallDisposition -Requested $settings.Recall -Plan $snapshot.Recall
$appOptions.ExtraId = @($appOptions.ExtraId | Where-Object { $inventory.Id -contains $_ }) $policyIssues=@($snapshot.Policies | Where-Object {$_.Status -in @('Conflict','ReadError')});$appIssues=@($snapshot.Apps | Where-Object {$_.Status -eq 'Blocked'})
$appOptions.ExcludeId = @($appOptions.ExcludeId | Where-Object { $inventory.Id -contains $_ }) $eligible=@($snapshot.Policies | Where-Object Status -in @('WouldChange','Configured') | ForEach-Object {$_.Id})
& (Join-Path $Root 'Manage-Apps.ps1') -Mode Plan -Browser $browsers @appOptions $skip=@($snapshot.Policies | Where-Object Status -eq 'Unsupported').Count;$count=@($snapshot.Apps | Where-Object Status -eq 'Remove').Count
$plan = @(Get-WindPlan -Context (Get-WindContext) -Store (New-WindRegistryStore) -Profile $Profile -Updates $Updates) $r=Select-WindMenu -Title 'Ready to apply?' -Subtitle "$count app removals / $($eligible.Count) policies / $(@($snapshot.Sensors).Count) sensors / $skip unsupported" -Items @((New-WindMenuItem 'cancel' 'Go back' 'Review selections or back up app data before continuing.'),(New-WindMenuItem 'apply' 'Apply the complete plan' 'This single approval authorizes selected app/account/Recall/cache deletion, sensor disablement and network changes. Apps and deleted data are not restored by policy rollback.'))
$plan | Select-Object Id,Status,Impact | Format-Table -Wrap | Out-Host if (-not $r.Accepted -or $r.Ids[0] -ne 'apply') {break}
if (@($plan | Where-Object Status -in @('Conflict','ReadError')).Count) { throw 'Resolve policy conflicts/read errors before applying.' } $tools=@();if($settings.InstallMullvad){$tools+='Mullvad'};if($settings.InstallVeraCrypt){$tools+='VeraCrypt'};$runState=[pscustomobject]@{NetworkJournal=$null};$groups=[Collections.Generic.List[object]]::new()
$eligible = @($plan | Where-Object Status -in @('WouldChange','Configured') | ForEach-Object Id) if($eligible.Count-or$policyIssues.Count){$groups.Add([pscustomobject]@{Id='Policies';Label='Registry policy writes';DependsOn=@();Action=({if($eligible.Count){& (Join-Path $Root 'ms-private.ps1') -Mode Apply -Profile $settings.Profile -Updates $settings.Updates -Id $eligible -Confirm:$false};if($policyIssues.Count){throw "$($policyIssues.Count) requested policies had a conflict or read error; eligible policies were attempted."}}.GetNewClosure())})}
if (@($plan | Where-Object Status -eq 'Unsupported').Count) { Write-Host 'Unsupported privacy policies will be skipped. Use G for the Home/manual guide.' -ForegroundColor Yellow } if($settings.Services){$groups.Add([pscustomobject]@{Id='Services';Label='Service configuration';DependsOn=@();Action=({& (Join-Path $Root 'Services.ps1') -Mode Apply -Profile Strict -Confirm:$false}.GetNewClosure())})}
Write-Host 'App uninstallers may remove app data. Policy journals do not restore apps. Back up your app data and make needed OneDrive files available locally.' if($settings.AccountPurge){$groups.Add([pscustomobject]@{Id='Accounts';Label='Credential and application-account purge';DependsOn=@();Action=({& (Join-Path $Root 'Accounts.ps1') -Mode Apply -Scope AllAppAccounts -AcknowledgeAccountDataDeletion -Confirm:$false}.GetNewClosure())})}
if ((Read-Host 'Type APPLY to execute this plan') -cne 'APPLY') { Write-Host 'Cancelled.'; break } if($tools.Count){$groups.Add([pscustomobject]@{Id='PrivacyTools';Label='Privacy-tool installation';DependsOn=@();Action=({& (Join-Path $Root 'Install-Apps.ps1') -App $tools -Confirm:$false}.GetNewClosure())})}
& (Join-Path $Root 'Manage-Apps.ps1') -Mode Apply -Browser $browsers @appOptions -Confirm:$false if(@($settings.Browsers).Count-or@($snapshot.Apps | 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."};& (Join-Path $Root 'Manage-Apps.ps1') -Mode Apply -Browser $settings.Browsers @options -ReviewedPlan $snapshot.Apps -Confirm:$false}.GetNewClosure())})}
if ($eligible.Count) { & (Join-Path $Root 'Wind.ps1') -Mode Apply -Profile $Profile -Updates $Updates -Id $eligible -Confirm:$false } if($settings.Network-ne'Observe'){$groups.Add([pscustomobject]@{Id='Network';Label='Firewall changes';DependsOn=@();Action=({$nr=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\network';[IO.Directory]::CreateDirectory($nr)|Out-Null;$runState.NetworkJournal=Join-Path $nr ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');& (Join-Path $Root 'Network.ps1') -Mode Apply -Profile $settings.Network -RollbackMinutes $settings.RollbackMinutes -Journal $runState.NetworkJournal -Confirm:$false}.GetNewClosure())})}
else { Write-Host 'App stage finished. Privacy policies were not applied on this edition/build; complete the manual guide.' } if(@($snapshot.Sensors).Count){$groups.Add([pscustomobject]@{Id='Sensors';Label='Exact device disablement';DependsOn=@();Action=({& (Join-Path $Root 'Hardware.ps1') -Mode Apply -InstanceId @($snapshot.Sensors.InstanceId) -Confirm:$false}.GetNewClosure())})}
Write-Host 'Review the result above, then reboot and check apps, updates and privacy settings.' $recallSkipped=$recallDisposition.Skipped.Count;if($settings.Recall-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.'};& (Join-Path $Root 'Recall.ps1') -Mode Apply -AcknowledgeSnapshotDeletion -RemoveFeature:$settings.RemoveRecall -Confirm:$false}.GetNewClosure())})}
if($settings.Cleanup-or$settings.PrivacyCleanup){$groups.Add([pscustomobject]@{Id='Cleanup';Label='Allowlisted file cleanup';DependsOn=@();Action=({if($settings.Cleanup){& (Join-Path $Root 'Cleanup.ps1') -Mode Run -AgeDays 7 -Category Temp -Confirm:$false};if($settings.PrivacyCleanup){& (Join-Path $Root 'Cleanup.ps1') -Mode Run -AgeDays 0 -Category @('CrashReports','RecentItems','ThumbnailCache','BrowserCache','PowerShellHistory') -ClearClipboard -Confirm:$false}}.GetNewClosure())})}
if($settings.CleanupSchedule){$groups.Add([pscustomobject]@{Id='CleanupSchedule';Label='Cleanup schedule';DependsOn=@();Action=({& (Join-Path $Root 'Cleanup.ps1') -Mode InstallSchedule -AgeDays 7 -Confirm:$false}.GetNewClosure())})}
if($settings.Lock){$groups.Add([pscustomobject]@{Id='LockHandler';Label='Session-lock handler';DependsOn=@();Action=({& (Join-Path $Root 'Lock.ps1') -Mode Install -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');$runResult=Invoke-MsPrivateGroupPlan -Groups @($groups) -ReportPath $runPath
$networkEntry=@($runResult.Entries | Where-Object { $_.Id -eq 'Network' });if($settings.Network-eq'Isolate'-and$networkEntry.Count-and$networkEntry[0].Status-eq'Succeeded'){Show-WindText 'Isolation safety timer active' @("Network isolation will roll back automatically unless explicitly confirmed with Network.ps1 -Mode Confirm -Journal `"$($runState.NetworkJournal)`".",'This preserves recovery if updates, activation or required access stop working.')}
$failed=@($runResult.Entries | Where-Object { $_.Status -in @('Failed','SkippedDependency') });Show-WindText 'Changes finished' @("Run status: $($runResult.Status). $($failed.Count) groups failed or were dependency-skipped; all other independent groups were attempted.","$skip unsupported policies and $recallSkipped unsupported Recall actions skipped.",'Reboot and check apps and security updates. Use the guide for skipped settings.',"Run report: $runPath",'Configuration is not proof of anonymity.')
} }
'7' { & (Join-Path $Root 'Wind.ps1') -Mode Audit -Profile $Profile -Updates $Updates } 'restore' {
'8' { $path=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\journals'
$journalRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'WindPrivacy\journals' $journals=@();if (Test-Path -LiteralPath $path) {$journals=@(Get-ChildItem -LiteralPath $path -Filter '*.journal.json' -File | Sort-Object LastWriteTime -Descending)}
$journals=@() if (-not $journals.Count) {Show-WindText 'Restore policies' @('No local policy journals.');break}
if (Test-Path -LiteralPath $journalRoot) { $journals=@(Get-ChildItem -LiteralPath $journalRoot -Filter '*.journal.json' -File | Sort-Object LastWriteTime -Descending) } $r=Select-WindMenu -Title 'Restore policies' -Subtitle 'Newest first / apps and data are not restored' -Items @($journals | ForEach-Object {New-WindMenuItem $_.FullName $_.Name 'Restore newer journals first. Conflicts preserve later edits.'})
if (-not $journals.Count) { Write-Host 'No local policy journals.'; break } if (-not $r.Accepted) {break};$journal=$r.Ids[0]
for ($i=0; $i -lt $journals.Count; $i++) { Write-Host ('{0} {1}' -f ($i+1),$journals[$i].Name) } $r=Select-WindMenu -Title 'Restore this journal?' -Subtitle ([IO.Path]::GetFileName($journal)) -Items @((New-WindMenuItem 'cancel' 'Cancel'),(New-WindMenuItem 'restore' 'Restore saved policy values'))
Write-Host 'Restore most recent journals first. This restores policies only, not removed apps or app data.' if ($r.Accepted -and $r.Ids[0] -eq 'restore') {& (Join-Path $Root 'ms-private.ps1') -Mode Restore -Journal $journal -Confirm:$false;Show-WindText 'Restore finished' @('Saved policy values restored. Reboot and verify.')}
$indices=@(ConvertFrom-WindChoice -Text (Read-Host 'Journal number, or blank to cancel') -Count $journals.Count)
if (-not $indices.Count) { break }
if ($indices.Count -ne 1) { throw 'Select one journal.' }
& (Join-Path $Root 'Wind.ps1') -Mode Restore -Journal $journals[$indices[0]].FullName -WhatIf
if ((Read-Host 'Type RESTORE to restore these policies') -ceq 'RESTORE') { & (Join-Path $Root 'Wind.ps1') -Mode Restore -Journal $journals[$indices[0]].FullName -Confirm:$false }
} }
'9' { Start-Process 'ms-settings:defaultapps' } 'defaults' {Start-Process 'ms-settings:defaultapps'}
'G' { & (Join-Path $Root 'Wind.ps1') -Mode Guide } 'guide' {Show-WindText 'Privacy guide' @(Get-Content -LiteralPath (Join-Path $Root 'docs/HOME-GUIDE.md') -Encoding UTF8)}
default { Write-Host 'Choose a menu item.' } 'gui' {Start-Process 'powershell.exe' -ArgumentList @('-NoProfile','-STA','-ExecutionPolicy','RemoteSigned','-File',(Join-Path $Root 'ms-private-GUI.ps1'))}
} }
} catch { Write-Host $_.Exception.Message -ForegroundColor Red } } catch {Show-WindText 'Action stopped' @($_.Exception.Message,'Check the run report and component journals. Completed groups remain applied; failed prerequisites are never reported as success.')}
} }
} }
Export-ModuleMember -Function Start-WindMenu,New-WindMenuState,Update-WindMenuState,Get-WindMenuFrame,New-WindMenuItem,Get-WindVisibleItems
Export-ModuleMember -Function Start-WindMenu,ConvertFrom-WindChoice
+68
View File
@@ -0,0 +1,68 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateScript({Test-Path -LiteralPath $_ -PathType Leaf})][string]$Path,
[Parameter(Mandatory)][ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version
)
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$resolved=[IO.Path]::GetFullPath($Path)
$bytes=[IO.File]::ReadAllBytes($resolved)
if($bytes.Length-lt 1024){throw 'Executable is unexpectedly small.'}
if($bytes[0]-ne0x4d-or$bytes[1]-ne0x5a){throw 'DOS header is missing.'}
$pe=[BitConverter]::ToInt32($bytes,0x3c)
if($pe-lt0-or($pe+96)-ge$bytes.Length-or[BitConverter]::ToUInt32($bytes,$pe)-ne0x00004550){throw 'PE header is invalid.'}
if([BitConverter]::ToUInt16($bytes,$pe+4)-ne0x8664){throw 'Executable is not AMD64.'}
$optional=$pe+24
if([BitConverter]::ToUInt16($bytes,$optional)-ne0x20b){throw 'Executable is not PE32+.'}
if([BitConverter]::ToUInt16($bytes,$optional+68)-ne2){throw 'Executable is not a Windows GUI application.'}
function Read-U16([byte[]]$Data,[int]$Offset){[BitConverter]::ToUInt16($Data,$Offset)}
function Read-U32([byte[]]$Data,[int]$Offset){[BitConverter]::ToUInt32($Data,$Offset)}
function Convert-Rva([byte[]]$Data,[int]$SectionTable,[int]$SectionCount,[uint32]$Rva){
for($index=0;$index-lt$SectionCount;$index++){
$section=$SectionTable+($index*40);$virtualSize=Read-U32 $Data ($section+8);$virtualAddress=Read-U32 $Data ($section+12);$rawSize=Read-U32 $Data ($section+16);$raw=Read-U32 $Data ($section+20)
$span=[Math]::Max([long]$virtualSize,[long]$rawSize)
if([long]$Rva-ge[long]$virtualAddress-and[long]$Rva-lt([long]$virtualAddress+$span)){return [int]([long]$raw+([long]$Rva-[long]$virtualAddress))}
}
throw 'Resource RVA is outside the PE sections.'
}
function Get-ResourceEntries([byte[]]$Data,[int]$Directory){
$count=(Read-U16 $Data ($Directory+12))+(Read-U16 $Data ($Directory+14));$rows=@()
for($index=0;$index-lt$count;$index++){$offset=$Directory+16+($index*8);$rows+=,[pscustomobject]@{Name=Read-U32 $Data $offset;Target=Read-U32 $Data ($offset+4)}}
$rows
}
function Get-IdEntry([byte[]]$Data,[int]$Directory,[uint32]$Id){
$match=@(Get-ResourceEntries $Data $Directory|Where-Object{([int64]$_.Name-band0x80000000L)-eq0-and($_.Name-band0xffff)-eq$Id})
if($match.Count-ne1){throw "PE resource ID $Id was not found exactly once."};$match[0]
}
$sectionCount=Read-U16 $bytes ($pe+6);$optionalSize=Read-U16 $bytes ($pe+20);$sectionTable=$optional+$optionalSize
$resourceRva=Read-U32 $bytes ($optional+128)
if($resourceRva-eq0){throw 'PE resource table is missing.'}
$resourceBase=Convert-Rva $bytes $sectionTable $sectionCount $resourceRva
$type=Get-IdEntry $bytes $resourceBase 24
if(([int64]$type.Target-band0x80000000L)-eq0){throw 'Manifest type does not reference a resource directory.'}
$nameDirectory=$resourceBase+[int]([int64]$type.Target-band0x7fffffffL)
$name=Get-IdEntry $bytes $nameDirectory 1
if(([int64]$name.Target-band0x80000000L)-eq0){throw 'Manifest name does not reference a language directory.'}
$languageDirectory=$resourceBase+[int]([int64]$name.Target-band0x7fffffffL)
$languages=@(Get-ResourceEntries $bytes $languageDirectory)
if($languages.Count-lt1-or([int64]$languages[0].Target-band0x80000000L)-ne0){throw 'Manifest language entry is invalid.'}
$dataEntry=$resourceBase+[int]([int64]$languages[0].Target-band0x7fffffffL)
$manifestRva=Read-U32 $bytes $dataEntry;$manifestSize=Read-U32 $bytes ($dataEntry+4)
$manifestOffset=Convert-Rva $bytes $sectionTable $sectionCount $manifestRva
if($manifestSize-eq0-or([long]$manifestOffset+$manifestSize)-gt$bytes.Length){throw 'Embedded application manifest is invalid.'}
$manifestBytes=[byte[]]::new($manifestSize);[Array]::Copy($bytes,$manifestOffset,$manifestBytes,0,$manifestSize)
$manifest=[Text.Encoding]::UTF8.GetString($manifestBytes)
if($manifest-notmatch'requestedExecutionLevel\s+level="requireAdministrator"'){throw 'Executable does not request administrator rights.'}
$info=[Diagnostics.FileVersionInfo]::GetVersionInfo($resolved)
if($info.FileVersion-notlike"$Version*"){throw "Unexpected file version: $($info.FileVersion)"}
$checksumPath=$resolved+'.sha256'
if(-not(Test-Path -LiteralPath $checksumPath -PathType Leaf)){throw 'Adjacent SHA-256 file is missing.'}
$expected=(([IO.File]::ReadAllText($checksumPath)-split'\s+')[0]).ToLowerInvariant()
$actual=(Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash.ToLowerInvariant()
if($expected-ne$actual){throw 'Executable SHA-256 does not match the adjacent checksum.'}
$signature=Get-AuthenticodeSignature -LiteralPath $resolved
if($signature.Status-notin@('NotSigned','Valid')){throw "Unexpected Authenticode status: $($signature.Status)"}
Write-Output "Executable inspection passed: AMD64 GUI, requireAdministrator, version $Version, SHA-256 $actual, signature $($signature.Status)."
+12
View File
@@ -0,0 +1,12 @@
#Requires -Version 5.1
$ErrorActionPreference='Stop'
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$files=@(Get-ChildItem -LiteralPath $root -Filter '*.ps1' -File)
foreach ($directory in @('src','tests')) {$files+=@(Get-ChildItem -LiteralPath (Join-Path $root $directory) -File | Where-Object Extension -in @('.ps1','.psm1'))}
foreach ($file in $files) {
$tokens=$null;$errors=$null
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors) | Out-Null
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)}
Write-Output 'All checks passed. No live policy or application mutations were performed.'
+102 -5
View File
@@ -3,6 +3,7 @@ Set-StrictMode -Version Latest
$ErrorActionPreference='Stop' $ErrorActionPreference='Stop'
Import-Module (Join-Path $PSScriptRoot '../src/Wind.Apps.psm1') -Force Import-Module (Join-Path $PSScriptRoot '../src/Wind.Apps.psm1') -Force
Import-Module (Join-Path $PSScriptRoot '../src/Wind.Tui.psm1') -Force Import-Module (Join-Path $PSScriptRoot '../src/Wind.Tui.psm1') -Force
Import-Module (Join-Path $PSScriptRoot '../src/MsPrivate.Presets.psm1') -Force
$script:passed=0 $script:passed=0
function Assert-True { param([bool]$Condition,[string]$Message) if (-not $Condition) { throw $Message } } function Assert-True { param([bool]$Condition,[string]$Message) if (-not $Condition) { throw $Message } }
function Assert-Throws { param([scriptblock]$Action) $threw=$false; try { & $Action | Out-Null } catch { $threw=$true }; Assert-True $threw 'Expected an exception.' } function Assert-Throws { param([scriptblock]$Action) $threw=$false; try { & $Action | Out-Null } catch { $threw=$true }; Assert-True $threw 'Expected an exception.' }
@@ -37,6 +38,29 @@ Run-Test 'Personal app opt-in and bundled exclusions' {
Assert-True (($plan | Where-Object Id -eq 'editor').Status -eq 'Remove') 'Personal selection ignored.' Assert-True (($plan | Where-Object Id -eq 'editor').Status -eq 'Remove') 'Personal selection ignored.'
Assert-True (($plan | Where-Object Id -eq 'app-1').Status -eq 'Keep') 'Exclusion ignored.' Assert-True (($plan | Where-Object Id -eq 'app-1').Status -eq 'Keep') 'Exclusion ignored.'
} }
Run-Test 'Aggressive presets select removable personal apps and Store' {
$store=New-App 'Microsoft.WindowsStore' 'Appx' 'store';$store.Protected=$true
$apps=@((New-App 'PersonalEditor' 'Desktop' 'editor'),(New-App 'Microsoft.WindowsNotepad' 'Appx' 'notepad'),$store,(New-App 'Bitdefender' 'Desktop' 'security'),(New-App 'Microsoft Edge WebView2 Runtime' 'Desktop' 'webview'))
$plan=@(Get-WindAppPlan -Inventory $apps -RemovePersonal -RemoveStore)
Assert-True (($plan|Where-Object {$_.Id -eq 'editor'}).Status-eq'Remove') 'Aggressive plan kept a removable personal app.'
Assert-True (($plan|Where-Object {$_.Id -eq 'store'}).Status-eq'Remove') 'Explicit Store removal was ignored.'
Assert-True (($plan|Where-Object {$_.Id -eq 'notepad'}).Status-eq'Remove') 'Removable Microsoft.Windows app was overprotected.'
Assert-True (@($plan|Where-Object{$_.Id-in@('security','webview')-and$_.Status-ne'Keep'}).Count-eq0) 'Aggressive plan selected a protected dependency or security product.'
}
Run-Test 'Aggressive presets skip ambiguous desktop registrations without blocking the run' {
$apps=@(New-App 'Duplicate Tool' 'Desktop' 'dup-a';New-App 'Duplicate Tool' 'Desktop' 'dup-b')
$plan=@(Get-WindAppPlan -Inventory $apps -RemovePersonal)
Assert-True (@($plan|Where-Object {$_.Status -ne 'Keep'}).Count-eq0) 'An ambiguous automatic selection was left actionable.'
Assert-True (@($plan|Where-Object {$_.Protected -and $_.Reason -like 'Skipped:*'}).Count-eq2) 'Ambiguous entries were not reported as protected skips.'
}
Run-Test 'Fresh Install and aggressive presets carry the complete destructive selection' {
$catalog=Get-MsPrivatePresetCatalog
foreach($id in @('FreshInstall','Medium','Strong','Maximum')){$p=Get-MsPrivatePreset $id;Assert-True ($p.RemovePersonal-and$p.RemoveStore-and$p.RemoveOneDrive-and$p.RemoveEdge-and$p.Recall-and$p.Accounts-and$p.Services) "Incomplete aggressive preset: $id"}
Assert-True ((Get-MsPrivatePreset 'Maximum').Network-eq'Isolate') 'Maximum preset lacks timed network isolation.'
Assert-True (@($catalog.Presets).Count-eq6) 'Unexpected preset count.'
$safe=Get-MsPrivatePreset 'EstablishedSafe'
Assert-True (-not$safe.Services-and-not$safe.Accounts-and-not$safe.RemoveBundled-and-not$safe.RemovePersonal-and-not$safe.RemoveStore-and-not$safe.RemoveOneDrive-and-not$safe.RemoveEdge-and-not$safe.Recall-and$safe.Network-eq'Observe'-and-not@($safe.Browsers).Count) 'EstablishedSafe selected a destructive or installation action.'
}
Run-Test 'Protected dependencies cannot be opted into removal' { Run-Test 'Protected dependencies cannot be opted into removal' {
foreach ($name in @('Microsoft Edge WebView2 Runtime','Microsoft.DesktopAppInstaller','Microsoft.WindowsStore','Microsoft.VCLibs.140.00','Bitdefender','Microsoft.GamingServices','NVIDIA Display Driver','Mullvad VPN','VeraCrypt')) { foreach ($name in @('Microsoft Edge WebView2 Runtime','Microsoft.DesktopAppInstaller','Microsoft.WindowsStore','Microsoft.VCLibs.140.00','Bitdefender','Microsoft.GamingServices','NVIDIA Display Driver','Mullvad VPN','VeraCrypt')) {
$app=New-App $name $app=New-App $name
@@ -116,10 +140,83 @@ Run-Test 'Forged protected plan is rejected before any action' {
Assert-Throws {Run-Changes $h} Assert-Throws {Run-Changes $h}
Assert-True ($h.State.Events.Count -eq 0) 'Forged plan executed.' Assert-True ($h.State.Events.Count -eq 0) 'Forged plan executed.'
} }
Run-Test 'Menu selection rejects invalid input and supports combinations' { Run-Test 'Arrow navigation wraps and Space toggles independently of Enter' {
Assert-True ((@(ConvertFrom-WindChoice '1,3,1' 3) -join ',') -eq '0,2') 'Multi-selection incorrect.' $items=@((New-WindMenuItem 'zen' 'Zen'),(New-WindMenuItem 'tor' 'Tor'))
Assert-True (@(ConvertFrom-WindChoice 'none' 3).Count -eq 0) 'None incorrect.' $s=New-WindMenuState @('zen')
Assert-True (@(ConvertFrom-WindChoice 'all' 3).Count -eq 3) 'All incorrect.' Update-WindMenuState $s $items 'UpArrow' -Multiple
foreach ($bad in @('0','4','-1','1;whoami','1.5')) {Assert-Throws {ConvertFrom-WindChoice $bad 3}} Assert-True ($s.Cursor -eq 1) 'Up did not wrap.'
Update-WindMenuState $s $items 'Spacebar' -Multiple
Assert-True ($s.Checked.Count -eq 2 -and $s.Action -eq '') 'Space accepted prematurely.'
Update-WindMenuState $s $items 'Enter' -Multiple
Assert-True ($s.Action -eq 'Accept') 'Enter did not accept.'
Update-WindMenuState $s $items 'Escape' -Multiple
Assert-True ($s.Action -eq 'Cancel') 'Escape did not cancel.'
}
Run-Test 'Filtering preserves checked apps and supports empty results' {
$items=@((New-WindMenuItem 'zen' 'Zen'),(New-WindMenuItem 'tor' 'Tor'))
$s=New-WindMenuState @('zen')
Update-WindMenuState $s $items 'T' ([char]'t') -Multiple -Searchable
Assert-True (@(Get-WindVisibleItems $items $s).Count -eq 1) 'Filter did not narrow list.'
Update-WindMenuState $s $items 'Spacebar' -Multiple
Assert-True ($s.Checked.Count -eq 2) 'Filter lost prior selections.'
Update-WindMenuState $s $items 'X' ([char]'x') -Searchable
Update-WindMenuState $s $items 'Enter'
Assert-True ($s.Action -eq '') 'Empty result accepted.'
Update-WindMenuState $s $items 'Backspace' -Searchable
Assert-True ($s.Filter -eq 't') 'Backspace failed.'
}
Run-Test 'Protected display rows never toggle or activate' {
$items=@(New-WindMenuItem 'protected' 'System component' '' $true);$s=New-WindMenuState
Update-WindMenuState $s $items 'Spacebar' -Multiple
Update-WindMenuState $s $items 'Enter'
Assert-True ($s.Checked.Count -eq 0 -and $s.Action -eq '') 'Disabled item activated.'
}
Run-Test 'Frames fit small terminals and page long app lists' {
$items=@(1..80 | ForEach-Object {New-WindMenuItem "app-$_" ('Long application name '+('x'*80)) ('Detailed information '*20)})
$s=New-WindMenuState; $s.Cursor=79
foreach ($width in @(40,80,120)) {
$frame=@(Get-WindMenuFrame -Title 'Apps' -Subtitle 'Select' -Items $items -State $s -Width $width -Height 24 -Multiple -Searchable)
Assert-True ($frame.Count -lt 24 -and @($frame | Where-Object {$_.Text.Length -ge $width}).Count -eq 0) 'Frame overflow.'
Assert-True (@($frame | Where-Object Active).Count -eq 1) 'Focus lost off-page.'
}
}
Run-Test 'Empty inventory can still install selected browsers' {
$h=New-Harness @();$r=Run-Changes $h
Assert-True ($r.Status -eq 'Completed' -and $h.State.Browser) 'Empty inventory broke install.'
}
Run-Test 'JSON catalogs flatten correctly in Windows PowerShell 5.1' {
$browsers=@(Get-WindBrowserCatalog)
Assert-True ($browsers.Count -eq 5 -and @($browsers | Where-Object Name -eq 'Zen').Count -eq 1) 'Nested catalog array.'
}
Run-Test 'Bundle parents suppress duplicate child app rows' {
$packages=@([pscustomobject]@{PackageFamilyName='family';IsBundle=$true;Name='parent'},[pscustomobject]@{PackageFamilyName='family';IsBundle=$false;Name='child'},[pscustomobject]@{PackageFamilyName='other';IsBundle=$false;Name='single'})
$parents=@(Select-WindAppxParents $packages)
Assert-True ($parents.Count -eq 2 -and $parents.Name -notcontains 'child') 'Bundle child duplicated.'
}
Run-Test 'Journal callback resolves inside nested module scopes' {
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$path=Join-Path $root ('reports/scope-'+[guid]::NewGuid().ToString('N')+'.json')
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($path)) | Out-Null
$nested=New-Module -ScriptBlock {}
try {
$h=New-Harness
& $nested {
param($Root,$Path,$Harness)
Import-Module (Join-Path $Root 'src/Wind.Core.psm1')
Import-Module (Join-Path $Root 'src/Wind.Apps.psm1')
$writer=Get-Command Save-WindJournal -CommandType Function
$save={param($report) & $writer -Journal $report -Path $Path}.GetNewClosure()
Invoke-WindAppChanges -Plan $Harness.Plan -Backend $Harness.Backend -Save $save -Confirm:$false | Out-Null
} $root $path $h
Assert-True ((Get-Content -LiteralPath $path -Raw | ConvertFrom-Json).Status -eq 'Completed') 'Nested journal writer failed.'
} finally {if ([IO.File]::Exists($path)) {[IO.File]::Delete($path)};Remove-Module $nested -ErrorAction SilentlyContinue}
}
Run-Test 'Group changes override prior group checkboxes while preserving personal choices' {
$inventory=@((New-App),(New-App 'PersonalEditor' 'Desktop' 'editor'))
$options=@{ExtraId=@('app-1','editor');ExcludeId=@();SkipBundled=$false;KeepOneDrive=$false;KeepEdge=$false}
Set-WindRemovalGroups -Options $options -Inventory $inventory -Bundled $false -OneDrive $true -Edge $true
$plan=@(Get-WindAppPlan -Inventory $inventory @options)
Assert-True (($plan | Where-Object Id -eq 'app-1').Status -eq 'Keep') 'Group removal toggle ignored.'
Assert-True (($plan | Where-Object Id -eq 'editor').Status -eq 'Remove') 'Personal choice lost.'
} }
Write-Output "$script:passed app/menu tests passed. Only synthetic inventories and backends were used." Write-Output "$script:passed app/menu tests passed. Only synthetic inventories and backends were used."
+135
View File
@@ -0,0 +1,135 @@
#Requires -Version 5.1
$ErrorActionPreference='Stop';Set-StrictMode -Version Latest
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
Import-Module (Join-Path $root 'src/Wind.Network.psm1') -Force
Import-Module (Join-Path $root 'src/Wind.Cleanup.psm1') -Force
Import-Module (Join-Path $root 'src/Wind.Lock.psm1') -Force
Import-Module (Join-Path $root 'src/Wind.Security.psm1') -Force
Import-Module (Join-Path $root 'src/MsPrivate.Services.psm1') -Force
Import-Module (Join-Path $root 'src/MsPrivate.Accounts.psm1') -Force
Import-Module (Join-Path $root 'src/Wind.Core.psm1') -Force
Import-Module (Join-Path $root 'src/MsPrivate.IO.psm1') -Force
Import-Module (Join-Path $root 'src/MsPrivate.Presets.psm1') -Force
Import-Module (Join-Path $root 'src/MsPrivate.Execution.psm1') -Force
$script:passed=0
function Assert-True([bool]$Condition,[string]$Message){if(-not$Condition){throw $Message};$script:passed++}
$ioRoot=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-feature-'+[guid]::NewGuid().ToString('N'))
try{
$jsonPath=Join-Path $ioRoot 'state.json'
Write-MsPrivateJsonAtomic -InputObject ([pscustomobject]@{Value=1}) -Path $jsonPath
Write-MsPrivateJsonAtomic -InputObject ([pscustomobject]@{Value=2}) -Path $jsonPath
$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.'
$network=Get-Content -LiteralPath (Join-Path $root 'config/network.json') -Raw|ConvertFrom-Json;$network.OptionalTelemetry[1].Fqdn=$network.OptionalTelemetry[0].Fqdn;$path=Join-Path $ioRoot 'network.json';$network|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-WindNetworkCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Network catalog accepted a duplicate FQDN.'
$network=Get-Content -LiteralPath (Join-Path $root 'config/network.json') -Raw|ConvertFrom-Json;$network.OptionalTelemetry[0].Fqdn='ctldl.windowsupdate.com';$path=Join-Path $ioRoot 'network-protected.json';$network|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-WindNetworkCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Network catalog accepted a protected Windows Update endpoint.'
$services=Get-Content -LiteralPath (Join-Path $root 'config/services.json') -Raw|ConvertFrom-Json;$services.Services[1].Id=$services.Services[0].Id;$path=Join-Path $ioRoot 'services.json';$services|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-MsPrivateServiceCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Service catalog accepted a duplicate ID.'
$accounts=Get-Content -LiteralPath (Join-Path $root 'config/accounts.json') -Raw|ConvertFrom-Json;$accounts.CredentialTargetPatterns=@($accounts.CredentialTargetPatterns)+@('[');$path=Join-Path $ioRoot 'accounts.json';$accounts|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-MsPrivateAccountCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Account catalog accepted an invalid credential pattern.'
$policies=@(Get-WindCatalog);$policies[1].Id=$policies[0].Id;$path=Join-Path $ioRoot 'policies.json';$policies|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-WindCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Policy catalog accepted a duplicate ID.'
$policies=@(Get-WindCatalog);$policies[0]|Add-Member -NotePropertyName Hive -NotePropertyValue 'Users' -Force;$path=Join-Path $ioRoot 'policies-hive.json';$policies|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-WindCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Policy catalog accepted an untrusted registry hive.'
$presets=Get-Content -LiteralPath (Join-Path $root 'config/presets.json') -Raw|ConvertFrom-Json;$presets.Presets[1].RemoveStore=$false;$path=Join-Path $ioRoot 'presets.json';$presets|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $path
$threw=$false;try{Get-MsPrivatePresetCatalog -Path $path|Out-Null}catch{$threw=$true};Assert-True $threw 'Preset catalog accepted an incomplete aggressive preset.'
$now=[datetime]'2026-09-22T12:00:00Z'
$fresh=Get-MsPrivateInstallContext -NowUtc $now -Probe {[pscustomobject]@{InstallDate=$now.AddDays(-2);ProfileCreationUtc=$now.AddDays(-1)}}
Assert-True ($fresh.Classification-eq'FreshInstall'-and$fresh.RecommendedPreset-eq'FreshInstall') 'A fresh OS and profile did not select FreshInstall.'
$upgraded=Get-MsPrivateInstallContext -NowUtc $now -Probe {[pscustomobject]@{InstallDate=$now.AddDays(-2);ProfileCreationUtc=$now.AddDays(-100)}}
Assert-True ($upgraded.Classification-eq'Established'-and$upgraded.RecommendedPreset-eq'EstablishedSafe') 'An older profile after a recent OS date was misclassified as fresh.'
$unknown=Get-MsPrivateInstallContext -NowUtc $now -Probe {throw 'unavailable'}
Assert-True ($unknown.Classification-eq'Unknown'-and$unknown.RecommendedPreset-eq'EstablishedSafe') 'Unknown installation age did not fail safe.'
}finally{if(Test-Path -LiteralPath $ioRoot){Remove-Item -LiteralPath $ioRoot -Recurse -Force}}
$observe=Get-WindNetworkPlan -Profile Observe
Assert-True ($observe.Items.Count-eq 1-and$observe.Items[0].Status-eq'Ready') 'Observe plan must be non-mutating.'
$threw=$false;try{Get-WindNetworkPlan -Profile Observe -RollbackMinutes 1|Out-Null}catch{$threw=$true};Assert-True $threw 'Network rollback lower bound was not enforced.'
$supportedRecall=Resolve-MsPrivateRecallDisposition -Requested $true -Plan ([pscustomobject]@{Items=@([pscustomobject]@{Id='recall-policy';Status='WouldChange'},[pscustomobject]@{Id='recall-feature';Status='Configured'})})
Assert-True ($supportedRecall.Applicable-and-not$supportedRecall.Conflict-and-not$supportedRecall.Skipped.Count) 'Supported Recall plan was not marked applicable.'
$unsupportedRecall=Resolve-MsPrivateRecallDisposition -Requested $true -Plan ([pscustomobject]@{Items=@([pscustomobject]@{Id='recall-policy';Status='Unsupported'})})
Assert-True (-not$unsupportedRecall.Applicable-and-not$unsupportedRecall.Conflict-and$unsupportedRecall.Skipped.Count-eq1) 'Unsupported Recall action was not reported as skipped.'
$conflictingRecall=Resolve-MsPrivateRecallDisposition -Requested $true -Plan ([pscustomobject]@{Items=@([pscustomobject]@{Id='recall-policy';Status='Conflict'})})
Assert-True (-not$conflictingRecall.Applicable-and$conflictingRecall.Conflict) 'Conflicting Recall plan did not remain a hard stop.'
$events=[Collections.Generic.List[string]]::new()
$groups=@(
[pscustomobject]@{Id='First';Label='First';DependsOn=@();Action=({$events.Add('first')|Out-Null;throw 'expected failure'}.GetNewClosure())},
[pscustomobject]@{Id='Dependent';Label='Dependent';DependsOn=@('First');Action=({$events.Add('dependent')|Out-Null}.GetNewClosure())},
[pscustomobject]@{Id='Independent';Label='Independent';DependsOn=@();Action=({$events.Add('independent')|Out-Null}.GetNewClosure())}
)
$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.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.'
$calls=[Collections.Generic.List[string]]::new()
$backend=@{
ReadProfiles={@([pscustomobject]@{Name='Domain';DefaultOutboundAction='Allow'},[pscustomobject]@{Name='Private';DefaultOutboundAction='Allow'},[pscustomobject]@{Name='Public';DefaultOutboundAction='Allow'})}
SetOutbound={param($name,$action)$calls.Add("set:${name}:$action")}
RemoveRules={param($g)$calls.Add("rules:$g")}
RemoveKeyword={param($id)$calls.Add("keyword:$id")}
RemoveRollback={param($n)$calls.Add("task:$n")}
}
$j=[pscustomobject]@{Schema=1;Kind='MsPrivateNetwork';Status='Applied';Profile='Isolate';RuleGroup='ms-private';KeywordIds=@();RollbackTask='ms-private Network Recovery 0123456789abcdef0123456789abcdef';Profiles=@([pscustomobject]@{Name='Domain';DefaultOutboundAction='Allow'},[pscustomobject]@{Name='Private';DefaultOutboundAction='Allow'},[pscustomobject]@{Name='Public';DefaultOutboundAction='Allow'})}
Restore-WindNetwork -Journal $j -Backend $backend -Save {param($x)}|Out-Null
Assert-True ($calls.Count-eq 4) 'Isolation restore did not restore three profiles and remove the task.'
Assert-True (Test-WindVeraCryptVolume 'X:') 'Valid VeraCrypt drive rejected.'
Assert-True (-not(Test-WindVeraCryptVolume 'C:\Windows')) 'Unsafe VeraCrypt target accepted.'
$threw=$false;try{Get-WindCleanupPlan -Root @((Join-Path $env:USERPROFILE 'Documents'))|Out-Null}catch{$threw=$true};Assert-True $threw 'Cleanup accepted a non-temp root.'
$cleanupCatalog=@(Get-WindPrivacyCleanupCatalog)
Assert-True (-not@($cleanupCatalog|Where-Object{$_.Root-match'(?i)winevt|System32\\LogFiles|Defender'}).Count) 'Cleanup catalog included protected event/security logs.'
Assert-True (-not@($cleanupCatalog|Where-Object{$_.Category-eq'WindReports'-and$_.Patterns-contains'*.journal.json'}).Count) 'Cleanup catalog included recovery journals.'
$browserPlan=Get-WindCleanupPlan -AgeDays 0 -Category BrowserCache
Assert-True ($browserPlan.Categories-contains'BrowserCache') 'Browser-cache cleanup category was not accepted.'
$browserRoots=@(Get-WindBrowserCacheCatalog);Assert-True (-not@($browserRoots|Where-Object{$_.Root-notmatch'(?i)\\(cache2|startupCache|Cache|Code Cache|GPUCache|DawnCache|GrShaderCache|ShaderCache|CacheStorage)$'}).Count) 'Browser cleanup included a non-cache profile path.'
$security=Get-WindSecurityStatus;Assert-True ($security.RecoveryKeysCaptured-eq$false) 'Security audit must never capture recovery keys.'
Assert-True ($security.SensitiveValuesCaptured-eq$false-and$security.Schema-eq2) 'Expanded security audit must remain secret-free and versioned.'
$serviceCatalog=Get-MsPrivateServiceCatalog
Assert-True (-not@($serviceCatalog.Services|Where-Object{$_.Name-in@($serviceCatalog.ProtectedServices)}).Count) 'Protected security/update service entered the privacy disable catalog.'
$serviceStates=@{DiagTrack=[pscustomobject]@{Exists=$true;StartExists=$true;Start=2;FlagsExists=$false;Flags=$null}}
$serviceCalls=[Collections.Generic.List[string]]::new();$serviceBackend=@{
ReadTemplate={param($name)if($serviceStates.ContainsKey($name)){$serviceStates[$name]}else{[pscustomobject]@{Exists=$false;StartExists=$false;Start=$null;FlagsExists=$false;Flags=$null}}}
WriteTemplate={param($name,$state)$serviceStates[$name]=[pscustomobject]@{Exists=$state.Exists;StartExists=$state.StartExists;Start=$state.Start;FlagsExists=$state.FlagsExists;Flags=$state.Flags};$serviceCalls.Add("write:${name}:$($state.Start)")}
ReadInstances={param($name,$instances)if($name-eq'DiagTrack'){@([pscustomobject]@{Name='DiagTrack';Status='Running'})}else{@()}}
StopInstance={param($name)$serviceCalls.Add("stop:$name")};StartInstance={param($name)$serviceCalls.Add("start:$name")}
}
$servicePlan=@(Get-MsPrivateServicePlan -Profile Strict -Backend $serviceBackend);$serviceJournal=Invoke-MsPrivateServiceApply -Plan $servicePlan -Backend $serviceBackend -Save {param($x)}
Assert-True ($serviceStates.DiagTrack.Start-eq4-and$serviceCalls-contains'stop:DiagTrack') 'Strict service plan did not disable and stop the telemetry service.'
$restored=Restore-MsPrivateServices -Journal $serviceJournal -Backend $serviceBackend -Save {param($x)}
Assert-True ($serviceStates.DiagTrack.Start-eq2-and$restored.Status-eq'Restored') 'Service journal did not restore original startup state.'
$forged=@(Get-MsPrivateServicePlan -Profile Strict -Backend $serviceBackend);$forged[0].Name='wuauserv';$threw=$false
try{Invoke-MsPrivateServiceApply -Plan $forged -Backend $serviceBackend -Save {param($x)}|Out-Null}catch{$threw=$true}
Assert-True $threw 'Service engine accepted a forged protected-service name.'
$accountCalls=[Collections.Generic.List[string]]::new();$accountBackend=@{
ListPackages={@([pscustomobject]@{Name='Microsoft.WindowsStore';PackageFullName='store_full'},[pscustomobject]@{Name='Microsoft.AAD.BrokerPlugin';PackageFullName='broker_full'})}
ResetPackage={param($name)$accountCalls.Add("reset:$name")}
ListCredentials={@([pscustomobject]@{TargetName='MicrosoftAccount:[email protected]';Type=[uint32]1},[pscustomobject]@{TargetName='unrelated-server';Type=[uint32]1})}
DeleteCredential={param($target,$type)$accountCalls.Add("delete:${target}:$type")}
}
$consumerPlan=@(Get-MsPrivateAccountPlan -Scope Consumer -Backend $accountBackend)
Assert-True (-not @($consumerPlan|Where-Object {$_.Id -eq 'web-account-broker'}).Count) 'Consumer account scope included the shared identity broker.'
$accountReport=Invoke-MsPrivateAccountPurge -Scope AllAppAccounts -Backend $accountBackend -Save {param($x)}
Assert-True ($accountReport.Status-eq'Completed'-and$accountCalls-contains'reset:store_full'-and$accountCalls-contains'reset:broker_full') 'Account purge did not reset exact installed package identities.'
Assert-True (@($accountCalls|Where-Object{$_-like'delete:MicrosoftAccount:*'}).Count-eq1-and-not@($accountCalls|Where-Object{$_-like'*unrelated-server*'}).Count) 'Account purge deleted outside its credential allowlist.'
Assert-True (-not@($accountReport.Entries|Where-Object{$_.PSObject.Properties['Key']}).Count) 'Account report retained a credential target.'
$accountReportPath=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-account-test-'+[guid]::NewGuid().ToString('N')+'.json')
try{Save-MsPrivateAccountReport -Report $accountReport -Path $accountReportPath;Save-MsPrivateAccountReport -Report $accountReport -Path $accountReportPath;$roundTrip=Get-Content -LiteralPath $accountReportPath -Raw|ConvertFrom-Json;Assert-True ($roundTrip.Status-eq'Completed') 'Account report atomic replacement failed.'}finally{[IO.File]::Delete($accountReportPath)}
[xml]$ame=Get-Content -LiteralPath (Join-Path $root 'ame/src/playbook.conf') -Raw
Assert-True ($ame.Playbook.Overhaul-eq'false') 'AME wrapper must not claim an OS overhaul.'
Assert-True (-not($ame.Playbook.Requirements.Requirement -contains 'DefenderDisabled')) 'AME wrapper must preserve Defender.'
$yaml=Get-Content -LiteralPath (Join-Path $root 'ame/src/Configuration/main.yml') -Raw
Assert-True ($yaml -match 'ms-private\\\\Launch\.cmd') 'AME launcher action missing.'
Write-Output "$script:passed feature tests passed. No OS setting was changed."
+29 -10
View File
@@ -13,7 +13,8 @@ function New-Harness {
Read = { Read = {
param($policy) param($policy)
if ($state.ReadFail) { throw 'Injected read error' } if ($state.ReadFail) { throw 'Injected read error' }
$key=$policy.Path+'|'+$policy.Name $hive=if($policy.PSObject.Properties['Hive']){[string]$policy.Hive}else{'LocalMachine'}
$key=$hive+'|'+$policy.Path+'|'+$policy.Name
if ($state.Values.ContainsKey($key)) { if ($state.Values.ContainsKey($key)) {
$v=$state.Values[$key] $v=$state.Values[$key]
return [pscustomobject]@{ Exists=$true; Kind=$v.Kind; Value=$v.Value } return [pscustomobject]@{ Exists=$true; Kind=$v.Kind; Value=$v.Value }
@@ -25,7 +26,8 @@ function New-Harness {
$state.Writes++ $state.Writes++
$fail=$state.FailName -eq $policy.Name $fail=$state.FailName -eq $policy.Name
if ($fail -and -not $state.FailAfterWrite) { $state.FailName=''; throw 'Injected write error' } if ($fail -and -not $state.FailAfterWrite) { $state.FailName=''; throw 'Injected write error' }
$key=$policy.Path+'|'+$policy.Name $hive=if($policy.PSObject.Properties['Hive']){[string]$policy.Hive}else{'LocalMachine'}
$key=$hive+'|'+$policy.Path+'|'+$policy.Name
if ($value.Exists) { $state.Values[$key]=[pscustomobject]@{ Kind=$value.Kind; Value=$value.Value } } if ($value.Exists) { $state.Values[$key]=[pscustomobject]@{ Kind=$value.Kind; Value=$value.Value } }
else { $state.Values.Remove($key) } else { $state.Values.Remove($key) }
if ($fail) { $state.FailName=''; throw 'Injected error after write' } if ($fail) { $state.FailName=''; throw 'Injected error after write' }
@@ -60,6 +62,13 @@ Run-Test 'Edition-aware diagnostic level' {
$h.Context.Edition='Enterprise' $h.Context.Edition='Enterprise'
Assert-True ((New-Plan $h @('diagnostics'))[0].Desired -eq 0) 'Enterprise should use supported minimum.' Assert-True ((New-Plan $h @('diagnostics'))[0].Desired -eq 0) 'Enterprise should use supported minimum.'
} }
Run-Test 'User-scoped and edition-scoped policies are planned honestly' {
$h=New-Harness
Assert-True ((New-Plan $h @('tailored-experiences'))[0].Desired -eq 1) 'Current-user tailored-experience policy missing.'
Assert-True (@(New-Plan $h @('windows-spotlight')).Count -eq 0) 'Pro must not claim the Enterprise-only Spotlight policy.'
$h.Context.Edition='Enterprise'
Assert-True ((New-Plan $h @('windows-spotlight'))[0].Desired -eq 1) 'Enterprise Spotlight policy was not selected.'
}
Run-Test 'Update choices are explicit and preserve update services' { Run-Test 'Update choices are explicit and preserve update services' {
$h=New-Harness $h=New-Harness
$keep=@(Get-WindPlan -Context $h.Context -Store $h.Store) $keep=@(Get-WindPlan -Context $h.Context -Store $h.Store)
@@ -74,13 +83,13 @@ Run-Test 'Unknown ID fails instead of silently ignoring a typo' {
} }
Run-Test 'Non-DWORD pre-existing value is never overwritten' { Run-Test 'Non-DWORD pre-existing value is never overwritten' {
$h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id' $h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id'
$h.State.Values[$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='String'; Value='custom' } $h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='String'; Value='custom' }
Assert-Throws { Apply-Test $h (New-Plan $h) } Assert-Throws { Apply-Test $h (New-Plan $h) }
Assert-True ($h.State.Writes -eq 0) 'Conflicting type was overwritten.' Assert-True ($h.State.Writes -eq 0) 'Conflicting type was overwritten.'
} }
Run-Test 'Microphone per-app overrides block misleading default-deny claim' { Run-Test 'Microphone per-app overrides block misleading default-deny claim' {
$h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'microphone-apps' $h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'microphone-apps'
$h.State.Values[$p.Path+'|LetAppsAccessMicrophone_ForceAllowTheseApps']=[pscustomobject]@{ Kind='MultiString'; Value=@('example.app') } $h.State.Values['LocalMachine|'+$p.Path+'|LetAppsAccessMicrophone_ForceAllowTheseApps']=[pscustomobject]@{ Kind='MultiString'; Value=@('example.app') }
Assert-True ((New-Plan $h @('microphone-apps'))[0].Status -eq 'Conflict') 'Exception was missed.' Assert-True ((New-Plan $h @('microphone-apps'))[0].Status -eq 'Conflict') 'Exception was missed.'
} }
Run-Test 'Unreadable registry is a read error, not missing or configured' { Run-Test 'Unreadable registry is a read error, not missing or configured' {
@@ -89,12 +98,12 @@ Run-Test 'Unreadable registry is a read error, not missing or configured' {
} }
Run-Test 'Apply and restore preserve absence and arbitrary original DWORD' { Run-Test 'Apply and restore preserve absence and arbitrary original DWORD' {
$h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id' $h=New-Harness; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id'
$h.State.Values[$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='DWord'; Value=7 } $h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='DWord'; Value=7 }
$j=Apply-Test $h (New-Plan $h) $j=Apply-Test $h (New-Plan $h)
Assert-True ($j.Status -eq 'Applied' -and $h.State.Values.Count -eq 2) 'Apply incomplete.' Assert-True ($j.Status -eq 'Applied' -and $h.State.Values.Count -eq 2) 'Apply incomplete.'
$r=Restore-Test $h ($j | ConvertTo-Json -Depth 12 | ConvertFrom-Json) $r=Restore-Test $h ($j | ConvertTo-Json -Depth 12 | ConvertFrom-Json)
Assert-True ($r.Status -eq 'Restored' -and $h.State.Values.Count -eq 1) 'Restore did not preserve absence.' Assert-True ($r.Status -eq 'Restored' -and $h.State.Values.Count -eq 1) 'Restore did not preserve absence.'
Assert-True ($h.State.Values[$p.Path+'|'+$p.Name].Value -eq 7) 'Original value lost.' Assert-True ($h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name].Value -eq 7) 'Original value lost.'
} }
Run-Test 'Reapplying configured policies makes zero registry writes' { Run-Test 'Reapplying configured policies makes zero registry writes' {
$h=New-Harness; Apply-Test $h (New-Plan $h) | Out-Null; $count=$h.State.Writes $h=New-Harness; Apply-Test $h (New-Plan $h) | Out-Null; $count=$h.State.Writes
@@ -119,10 +128,10 @@ Run-Test 'Failure after mutation also rolls back the current setting' {
Run-Test 'External edits cause restore conflict and are preserved' { Run-Test 'External edits cause restore conflict and are preserved' {
$h=New-Harness; $j=Apply-Test $h (New-Plan $h) $h=New-Harness; $j=Apply-Test $h (New-Plan $h)
$p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id' $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id'
$h.State.Values[$p.Path+'|'+$p.Name].Value=9 $h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name].Value=9
$r=Restore-Test $h $j $r=Restore-Test $h $j
Assert-True ($r.Status -eq 'RestoreIncomplete') 'Conflict not reported.' Assert-True ($r.Status -eq 'RestoreIncomplete') 'Conflict not reported.'
Assert-True ($h.State.Values[$p.Path+'|'+$p.Name].Value -eq 9) 'External edit overwritten.' Assert-True ($h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name].Value -eq 9) 'External edit overwritten.'
} }
Run-Test 'Crash with Writing journal status remains recoverable' { Run-Test 'Crash with Writing journal status remains recoverable' {
$h=New-Harness; $j=Apply-Test $h (New-Plan $h); $j.Entries[0].Status='Writing' $h=New-Harness; $j=Apply-Test $h (New-Plan $h); $j.Entries[0].Status='Writing'
@@ -153,9 +162,9 @@ Run-Test 'Journal rejects wrong installation and invalid target' {
} }
Run-Test 'State change between planning and apply is not overwritten' { Run-Test 'State change between planning and apply is not overwritten' {
$h=New-Harness; $plan=New-Plan $h; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id' $h=New-Harness; $plan=New-Plan $h; $p=Get-WindCatalog | Where-Object Id -EQ 'advertising-id'
$h.State.Values[$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='DWord'; Value=88 } $h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name]=[pscustomobject]@{ Kind='DWord'; Value=88 }
Assert-Throws { Apply-Test $h $plan } Assert-Throws { Apply-Test $h $plan }
Assert-True ($h.State.Writes -eq 0 -and $h.State.Values[$p.Path+'|'+$p.Name].Value -eq 88) 'Stale plan wrote registry.' Assert-True ($h.State.Writes -eq 0 -and $h.State.Values['LocalMachine|'+$p.Path+'|'+$p.Name].Value -eq 88) 'Stale plan wrote registry.'
} }
Run-Test 'Journal failure after a successful write triggers recovery' { Run-Test 'Journal failure after a successful write triggers recovery' {
$h=New-Harness; $h.State.FailSaveAt=3 $h=New-Harness; $h.State.FailSaveAt=3
@@ -184,4 +193,14 @@ Run-Test 'Durable journal creation and replacement round-trip' {
} }
} }
Run-Test 'Switching Strict to Standard releases strict-only restrictions' {
$h=New-Harness
$strict=@(Get-WindPlan -Context $h.Context -Store $h.Store -Profile Strict)
Apply-Test $h $strict | Out-Null
$standard=@(Get-WindPlan -Context $h.Context -Store $h.Store -Profile Standard)
Assert-True (($standard | Where-Object Id -eq 'clipboard-history').Desired -eq 1) 'Standard left history disabled.'
Assert-True (($standard | Where-Object Id -eq 'delivery-optimization').Desired -eq 0) 'Standard retained strict DO mode.'
Apply-Test $h $standard | Out-Null
Assert-True (@(Get-WindPlan -Context $h.Context -Store $h.Store -Profile Standard | Where-Object Status -ne 'Configured').Count -eq 0) 'Mode change incomplete.'
}
Write-Output "$script:passed tests passed. No Windows policy settings were modified." Write-Output "$script:passed tests passed. No Windows policy settings were modified."