91 lines
5.3 KiB
PowerShell
91 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 = 'Standard',
|
|
[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') -Force
|
|
|
|
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 Wind menu; no settings or applications changed.'; return }
|
|
Import-Module (Join-Path $PSScriptRoot 'src/Wind.Tui.psm1') -Force
|
|
Start-WindMenu -Root $PSScriptRoot -Profile $Profile -Updates $Updates
|
|
return
|
|
}
|
|
|
|
if ($Mode -eq 'Guide') {
|
|
Get-Content -LiteralPath (Join-Path $PSScriptRoot 'docs/HOME-GUIDE.md')
|
|
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')) 'WindPrivacy'
|
|
$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\WindPrivacyPolicyTransaction')
|
|
$locked = $false
|
|
try {
|
|
try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true }
|
|
if (-not $locked) { throw 'Another Wind 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
|
|
}
|
|
$save = { param($data) Save-WindJournal -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()
|
|
}
|