Files

74 lines
9.2 KiB
PowerShell

#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.'}