Files
ms-private/ms-private.ps1
T

92 lines
5.3 KiB
PowerShell

#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()
}