#Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('Menu','Audit','Plan','Apply','Restore','Guide')][string]$Mode = 'Menu', [ValidateSet('Standard','Strict')][string]$Profile = 'Strict', [ValidateSet('Keep','Automatic','Notify','Manual')][string]$Updates = 'Keep', [string[]]$Id = @(), [string]$Journal, [switch]$Json ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1') 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 ($WhatIfPreference) { Write-Output 'Preview: launch ms-private menu; no settings or applications changed.'; return } Import-Module (Join-Path $PSScriptRoot 'src/Wind.Tui.psm1') Start-WindMenu -Root $PSScriptRoot -Profile $Profile -Updates $Updates return } if ($Mode -eq 'Guide') { Get-Content -LiteralPath (Join-Path $PSScriptRoot 'docs/HOME-GUIDE.md') -Encoding UTF8 return } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } $context = Get-WindContext $store = New-WindRegistryStore $plan = @(Get-WindPlan -Context $context -Store $store -Profile $Profile -Updates $Updates -Id $Id) if ($Mode -in @('Audit','Plan')) { if ($Json) { [pscustomobject]@{ Schema=1; Context=$context; Profile=$Profile; Updates=$Updates; Verification='Policy configuration only, not network behavior or anonymity'; Policies=$plan } | ConvertTo-Json -Depth 10 } else { Write-Output "Windows edition: $($context.Edition); build: $($context.Build).$($context.Revision)" Write-Output 'Configured means registry configuration only, not verified enforcement or anonymity.' $plan | Select-Object Id,Status,Desired,Impact | Format-Table -Wrap } return } $stateRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate' $markerPath = Join-Path $stateRoot 'installation-id.txt' if ($Mode -eq 'Restore') { if ([string]::IsNullOrWhiteSpace($Journal)) { throw 'Restore requires -Journal.' } if (-not (Test-Path -LiteralPath $markerPath)) { throw 'Local installation marker is missing; inspect the journal manually.' } $installationId = (Get-Content -LiteralPath $markerPath -Raw).Trim() $saved = Get-Content -LiteralPath $Journal -Raw | ConvertFrom-Json Assert-WindJournal $saved $installationId $saved.Entries | Select-Object Id,Status | Format-Table } else { if ($Journal) { throw '-Journal is only accepted for Restore. Apply creates a fresh journal.' } if ($plan.Count -eq 0) { throw 'No active settings selected for this profile/update choice.' } $plan | Select-Object Id,Status,Desired,Impact | Format-Table -Wrap if (@($plan | Where-Object Status -NotIn @('WouldChange','Configured')).Count) { throw 'Plan contains unsupported/conflicting/unreadable policies. Review Guide or select supported IDs. No changes made.' } if (@($plan | Where-Object Status -EQ 'WouldChange').Count -eq 0) { Write-Output 'Selected policy values already match; no writes or journal needed.'; return } } if (-not $PSCmdlet.ShouldProcess('Local Windows machine policies', "$Mode selected privacy settings")) { return } Assert-WindMutationAllowed $mutex = [Threading.Mutex]::new($false, 'Global\MsPrivatePolicyTransaction') $locked = $false try { try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true } if (-not $locked) { throw 'Another ms-private transaction is running.' } if ($Mode -eq 'Apply') { # 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) if (@($plan | Where-Object Status -NotIn @('WouldChange','Configured')).Count) { throw 'Policy state changed; regenerate the plan.' } $journalRoot = Join-Path $stateRoot 'journals' [IO.Directory]::CreateDirectory($journalRoot) | Out-Null if (-not (Test-Path -LiteralPath $markerPath)) { [IO.File]::WriteAllText($markerPath, [guid]::NewGuid().ToString()) } $installationId = (Get-Content -LiteralPath $markerPath -Raw).Trim() $Journal = Join-Path $journalRoot ((Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N') + '.journal.json') } else { # Re-read under the lock; all input must pass validation again. $saved = Get-Content -LiteralPath $Journal -Raw | ConvertFrom-Json Assert-WindJournal $saved $installationId } $journalWriter = Get-Command Save-WindJournal -CommandType Function $save = { param($data) & $journalWriter -Journal $data -Path $Journal }.GetNewClosure() Write-Output "Recovery journal: $Journal" 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 } Write-Output "Transaction status: $($result.Status). Reboot and verify behavior before relying on these settings." if ($result.Status -eq 'RestoreIncomplete') { throw 'Some settings could not be restored; inspect journal entry statuses.' } } finally { if ($locked) { $mutex.ReleaseMutex() } $mutex.Dispose() }