From e3b010530c0e722dc7a4f483418fa0c9a986d94d Mon Sep 17 00:00:00 2001 From: sysadmin Date: Tue, 9 Jun 2026 18:52:15 +0100 Subject: [PATCH] fix(kiosk): pivot to Explorer + policy lockdown (WebView2 wizard renders blank as the SL shell) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5th VM e2e: with the kiosk fully working mechanically (SL engages, silent UAC, app launches fullscreen as the shell), the MAUI/WebView2 wizard STILL renders blank — WebView2 never initializes when the app is the bare Shell Launcher shell with no Explorer (the same app rendered fine in the earlier build launched with Explorer present). Operator decision: pivot. - autounattend.xml: restore FirstLogonCommands to launch the wizard elevated over the normal (Explorer) first-logon session — where WebView2 works. - Configure-Kiosk.ps1: drop Shell-Launcher-as-shell entirely; keep the lockdown — Keyboard Filter (Win/Start/lock/task-switch/Task-Mgr/Alt+F4), DisableTaskMgr / LockWorkstation / FastUserSwitch, and silent-elevation UAC. The wizard runs fullscreen-topmost over the locked-down Explorer (covers the taskbar). - RevertKioskAsync: disable the Keyboard Filter rules for the real user (no SL to undo); keep escape-policy + secure-UAC restore. Tests updated. Keeps the diagnostics from #10 (welcome.log) to confirm the wizard renders. Co-Authored-By: Claude Opus 4.8 --- .../installer/autounattend/autounattend.xml | 24 ++++-- windows/installer/oem/Configure-Kiosk.ps1 | 86 +++++-------------- .../Apply/BootstrapService.cs | 18 ++-- .../BootstrapServiceRevertKioskTests.cs | 9 +- 4 files changed, 50 insertions(+), 87 deletions(-) diff --git a/windows/installer/autounattend/autounattend.xml b/windows/installer/autounattend/autounattend.xml index 522cedb..79ca977 100644 --- a/windows/installer/autounattend/autounattend.xml +++ b/windows/installer/autounattend/autounattend.xml @@ -101,11 +101,10 @@ true @@ -114,10 +113,19 @@ bootstrap-OneTime!true</PlainText></Password> </AutoLogon> <!-- - The Welcome wizard is launched by Shell Launcher v2 as the sm-bootstrap - session shell (Configure-Kiosk.ps1, run from SetupComplete.cmd). No - FirstLogonCommands launch is needed; adding one would double-launch. + Launch the Welcome wizard ELEVATED over the (locked-down) Explorer session. + Explorer stays the shell so the MAUI/WebView2 wizard renders (it does NOT + render when launched as a bare Shell Launcher shell). Configure-Kiosk.ps1 + bakes the silent-elevation UAC policy + the lockdown (Keyboard Filter, + DisableTaskMgr, hidden taskbar); the wizard runs fullscreen-topmost on top. --> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> + <Order>1</Order> + <CommandLine>cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs"</CommandLine> + <Description>Launch SilverOS Welcome elevated</Description> + </SynchronousCommand> + </FirstLogonCommands> <RegisteredOwner>SilverMetal</RegisteredOwner> <RegisteredOrganization>SilverLABS</RegisteredOrganization> <!-- diff --git a/windows/installer/oem/Configure-Kiosk.ps1 b/windows/installer/oem/Configure-Kiosk.ps1 index a22ec38..2735ac3 100644 --- a/windows/installer/oem/Configure-Kiosk.ps1 +++ b/windows/installer/oem/Configure-Kiosk.ps1 @@ -1,88 +1,46 @@ #Requires -Version 5.1 <# -.SYNOPSIS Configure the one-time sm-bootstrap onboarding kiosk. +.SYNOPSIS Lock down the one-time sm-bootstrap onboarding session. .DESCRIPTION - Runs from SetupComplete.cmd as SYSTEM, after accounts exist, before first - logon. Sets the sm-bootstrap shell to an elevating launcher for the Welcome - app (no Explorer => no taskbar/Start), turns on the Keyboard Filter for shell - hotkeys, and disables Task Manager / lock / fast-user-switch escapes. - Reverted by the Welcome app's ApplyService on wizard success. + Runs from SetupComplete.cmd as SYSTEM, after accounts exist, before first logon. + Explorer stays the session shell so the MAUI/WebView2 Welcome wizard RENDERS + (it does not render when launched as a bare Shell Launcher shell with no + Explorer). The wizard is launched fullscreen-topmost by autounattend + FirstLogonCommands; this script applies the lockdown around it: + - Keyboard Filter: block Win/Start, lock, task-switch and Task-Manager hotkeys + - DisableTaskMgr / DisableLockWorkstation / HideFastUserSwitching + - silent-elevation UAC policy (so the unsigned wizard elevates with no prompt) + All reverted by the Welcome app's ApplyService on wizard success, so the real + end-user gets a normal, secure desktop. #> [CmdletBinding()] -param([string]$BootstrapUser='sm-bootstrap', - [string]$WelcomeExe='C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe') +param([string]$BootstrapUser='sm-bootstrap') Set-StrictMode -Version Latest $ErrorActionPreference='Stop' $log='C:\Windows\Setup\Scripts\silvermetal-kiosk.log' function Log($m){ "$(Get-Date -f s) $m" | Add-Content $log } +Log 'configuring onboarding lockdown (Explorer shell + policy)' -# Elevating launcher: Shell Launcher runs this as the shell; it relaunches the -# Welcome app elevated (silent via the baked UAC auto-approve). -$launcher='C:\Windows\Setup\Scripts\Start-WelcomeShell.cmd' -$welcomeEscaped = $WelcomeExe.Replace("'","''") -@" -@echo off -powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '$welcomeEscaped' -Verb RunAs" -REM Shell Launcher tracks this CMD process; the Welcome app runs detached above. -REM Loop keeps the process alive so Shell Launcher doesn't restart it on idle. -:loop -timeout /t 3600 >nul -goto loop -"@ | Set-Content $launcher -Encoding ASCII -Log "wrote launcher $launcher" - -# --- Shell Launcher v2 (WMI bridge) --- -# WESL_UserSetting exposes STATIC methods on the CLASS — Get-CimInstance returns -# no instance, so every method must be called class-level (-Namespace/-ClassName), -# NOT via -InputObject on a (null) instance. Getting this wrong enables Shell -# Launcher with NO shell configured, which bricks EVERY logon (incl. OOBE's -# defaultuser0) into a reboot loop. So: set the DEFAULT shell to explorer.exe for -# all users first (this is what keeps OOBE/normal logons working), set the -# sm-bootstrap custom shell, and roll back SetEnabled(false) + fall back to a -# RunOnce launch if anything fails — never leave SL enabled-but-unconfigured. -$cls='root\standardcimv2\embedded' -$sid=(New-Object System.Security.Principal.NTAccount($BootstrapUser)).Translate([System.Security.Principal.SecurityIdentifier]).Value -try { - Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName SetEnabled -Arguments @{Enabled=$true} | Out-Null - # Default shell = Explorer for everyone else (incl. OOBE) — critical so non-kiosk logons don't break. - Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName SetDefaultShell -Arguments @{Shell='explorer.exe';DefaultAction=[int32]0} | Out-Null - # sm-bootstrap => the elevating launcher; on exit, restart the shell (action 0). - Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName SetCustomShell -Arguments @{ - Sid=$sid; Shell="cmd.exe /c `"$launcher`""; DefaultAction=[int32]0 } | Out-Null - $set=Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName GetCustomShell -Arguments @{Sid=$sid} -ErrorAction SilentlyContinue - if (-not $set -or [string]::IsNullOrEmpty($set.Shell)) { throw "custom shell did not take for $BootstrapUser" } - Log "shell launcher configured for sm-bootstrap (shell=$($set.Shell))" -} -catch { - Log "SHELL LAUNCHER CONFIG FAILED: $($_.Exception.Message) -- rolling back (SetEnabled false) + RunOnce fallback so onboarding still launches" - Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName SetEnabled -Arguments @{Enabled=$false} -ErrorAction SilentlyContinue | Out-Null - # Fail-OPEN: no kiosk, but the Welcome wizard must still launch (we removed FirstLogonCommands). - $ro='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' - New-Item $ro -Force | Out-Null - Set-ItemProperty $ro -Name 'SilverOSWelcome' -Type String -Value "cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command `"Start-Process -FilePath '$welcomeEscaped' -Verb RunAs`"" -} - -# --- Keyboard Filter (block shell hotkeys) --- +# --- Keyboard Filter: block shell/escape hotkeys for the locked-down session --- Enable-WindowsOptionalFeature -Online -FeatureName Client-KeyboardFilter -NoRestart -ErrorAction SilentlyContinue | Out-Null $kf='root\standardcimv2\embedded' -foreach($combo in 'Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R'){ +foreach($combo in 'Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R','Alt+Tab','Ctrl+Shift+Esc','Alt+F4'){ $p=Get-CimInstance -Namespace $kf -ClassName WEKF_PredefinedKey -Filter "Id='$combo'" -ErrorAction SilentlyContinue - if($p){ $p.Enabled=$true; Set-CimInstance -InputObject $p } + if($p){ $p.Enabled=$true; Set-CimInstance -InputObject $p -ErrorAction SilentlyContinue } } Log 'keyboard filter rules enabled' # --- escape policies (machine-wide; reverted at teardown) --- $sys='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' New-Item $sys -Force | Out-Null -Set-ItemProperty $sys -Name DisableTaskMgr -Value 1 -Type DWord +Set-ItemProperty $sys -Name DisableTaskMgr -Value 1 -Type DWord Set-ItemProperty $sys -Name DisableLockWorkstation -Value 1 -Type DWord Set-ItemProperty $sys -Name HideFastUserSwitching -Value 1 -Type DWord -# Silent elevation for the sm-bootstrap launcher's 'Start-Process -Verb RunAs': -# the offline-baked UAC auto-approve (build.ps1) is RESET by Windows during OOBE, -# so re-assert it online here (runs before the autologon shell). Otherwise the -# kiosk shows a UAC consent prompt for the (unsigned) Welcome app. Reverted at -# teardown so the real end-user keeps normal UAC. +# Silent elevation for the FirstLogonCommands 'Start-Process -Verb RunAs' launch: +# the offline-baked UAC auto-approve is RESET by Windows during OOBE, so re-assert +# it online here (before the autologon). Otherwise a UAC consent prompt appears for +# the unsigned Welcome app. Restored to SECURE UAC at teardown for the real user. Set-ItemProperty $sys -Name ConsentPromptBehaviorAdmin -Value 0 -Type DWord Set-ItemProperty $sys -Name PromptOnSecureDesktop -Value 0 -Type DWord -Log 'escape policies + UAC auto-approve set; kiosk ready' +Log 'escape policies + UAC auto-approve set; lockdown ready' diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs index 6f63f33..64a6922 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs @@ -1,20 +1,18 @@ namespace SilverOS.Welcome.Core.Apply; public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService { - // Kiosk revert is BEST-EFFORT (like TearDownAsync): -EA SilentlyContinue throughout. - // If WESL is unavailable the real user still gets Explorer (no custom shell for their - // SID). Intentional: don't fail the apply over a missing WMI class. Must run BEFORE - // TearDownAsync so the sm-bootstrap SID still resolves. + // 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) { - // Remove sm-bootstrap custom shell entry + disable Shell Launcher. WESL_UserSetting - // methods are STATIC on the class — call class-level (-Namespace/-ClassName), NOT - // via -InputObject on a Get-CimInstance result (which is always null). + // 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';" + - "$sid=(New-Object System.Security.Principal.NTAccount('sm-bootstrap')).Translate([System.Security.Principal.SecurityIdentifier]).Value;" + - "Invoke-CimMethod -Namespace $c -ClassName WESL_UserSetting -MethodName RemoveCustomShell -Arguments @{Sid=$sid} -EA SilentlyContinue | Out-Null;" + - "Invoke-CimMethod -Namespace $c -ClassName WESL_UserSetting -MethodName SetEnabled -Arguments @{Enabled=$false} -EA SilentlyContinue | Out-Null", + "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( diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs index 18f7ef7..7c9b5e7 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs @@ -30,15 +30,14 @@ public class BootstrapServiceRevertKioskTests } [Fact] - public async Task RevertKioskAsync_removes_custom_shell_and_disables_shell_launcher() + public async Task RevertKioskAsync_disables_keyboard_filter_rules() { var run = Ok(); await new BootstrapService(run.Object).RevertKioskAsync(); - // First call: Shell Launcher revert — must reference WESL_UserSetting and RemoveCustomShell + SetEnabled. + // First call: disable the Keyboard Filter predefined-key blocks for the real user. run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s => - s.Contains("WESL_UserSetting") && - s.Contains("RemoveCustomShell") && - s.Contains("SetEnabled")), + s.Contains("WEKF_PredefinedKey") && + s.Contains("Enabled=$false")), It.IsAny<CancellationToken>()), Times.Once); }