namespace SilverOS.Welcome.Core.Apply; public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService { // Lockdown revert is BEST-EFFORT (like TearDownAsync): -EA SilentlyContinue throughout. // Don't fail the apply over a missing WMI class / key. Must run BEFORE TearDownAsync. public async Task RevertKioskAsync(CancellationToken ct = default) { // Disable the Keyboard Filter rules so the real end-user's Win key / task-switch / // Alt+F4 etc. work again (Explorer is already the shell — nothing to undo there). await Ps( "$c='root\\\\standardcimv2\\\\embedded';" + "foreach($k in @('Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R','Alt+Tab','Ctrl+Shift+Esc','Alt+F4')){" + "$p=Get-CimInstance -Namespace $c -ClassName WEKF_PredefinedKey -Filter \"Id='$k'\" -EA SilentlyContinue;" + "if($p){$p.Enabled=$false; Set-CimInstance -InputObject $p -EA SilentlyContinue}" + "}", ct); // Revert escape policies set by Configure-Kiosk.ps1. await Ps( "$s='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System';" + "Remove-ItemProperty $s -Name DisableTaskMgr,DisableLockWorkstation,HideFastUserSwitching -EA SilentlyContinue;" + // Restore SECURE UAC for the real end-user (the kiosk auto-approved unsigned elevation). "Set-ItemProperty $s -Name ConsentPromptBehaviorAdmin -Value 2 -Type DWord -EA SilentlyContinue;" + "Set-ItemProperty $s -Name PromptOnSecureDesktop -Value 1 -Type DWord -EA SilentlyContinue", ct); } // Teardown is BEST-EFFORT (unlike Account/BitLocker which are strict): the answer file's // AutoLogon LogonCount=1 already neutralises auto-logon after the first logon (Windows clears // AutoAdminLogon itself), so these Winlogon cleanups must not fail the whole apply. The op that // matters — removing the sm-bootstrap account — runs regardless and is tolerant too. public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default) { const string w = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'"; await Ps($"Set-ItemProperty -Path {w} -Name AutoAdminLogon -Value '0' -EA SilentlyContinue; " + $"Remove-ItemProperty -Path {w} -Name DefaultPassword -EA SilentlyContinue; " + $"Remove-ItemProperty -Path {w} -Name DefaultUserName -EA SilentlyContinue; " + $"Remove-ItemProperty -Path {w} -Name DefaultDomainName -EA SilentlyContinue", ct); var u = Esc(bootstrapUser); // Best-effort in-session removal (usually no-ops — you can't delete the account // you're logged in as), THEN defer the real removal to a SYSTEM startup task that // runs on next boot, when sm-bootstrap is no longer logged on. It removes the // account + profile, then unregisters itself. // Disable immediately (in-session, takes effect at once so the account is unusable // and shows as disabled), then best-effort delete; the deferred task does the real // delete on next boot when it isn't logged on. await Ps($"Disable-LocalUser -Name '{u}' -EA SilentlyContinue; Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct); var cleanup = $"Remove-LocalUser -Name '{u}' -ErrorAction SilentlyContinue; " + $"Get-CimInstance Win32_UserProfile | Where-Object {{ $_.LocalPath -like '*\\{u}' }} | Remove-CimInstance -ErrorAction SilentlyContinue; " + "Unregister-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Confirm:$false -ErrorAction SilentlyContinue"; var b64 = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(cleanup)); // Register-ScheduledTask (not schtasks.exe) — schtasks /tr caps at 261 chars and // silently failed with the encoded payload, so the task was never created. await Ps("$a=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument " + $"'-NoProfile -ExecutionPolicy Bypass -EncodedCommand {b64}'; " + "$t=New-ScheduledTaskTrigger -AtStartup; " + "$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest; " + "Register-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Action $a -Trigger $t -Principal $p -Force | Out-Null", ct); } private static string Esc(string s) => s.Replace("'", "''"); private Task Ps(string s, CancellationToken ct) => runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{s}\"", ct); }