From 7e99d7e3043142408f8d51f266f26b055220cec8 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 10 Jun 2026 10:34:47 +0100 Subject: [PATCH] fix(collector): carry preconfig via chunked FirstLogonCommands (specialize Path was too long -> answer file invalid) --- windows/collector/New-SmAnswerFile.ps1 | 64 ++++++++++++++++++-------- windows/tests/Collector.Tests.ps1 | 19 ++++---- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/windows/collector/New-SmAnswerFile.ps1 b/windows/collector/New-SmAnswerFile.ps1 index d0cc6a2..06b8f89 100644 --- a/windows/collector/New-SmAnswerFile.ps1 +++ b/windows/collector/New-SmAnswerFile.ps1 @@ -1,8 +1,11 @@ #Requires -Version 5.1 # Pure generator: collected values -> Windows Setup answer-file XML string. # No WinForms dependency (unit-testable). Mirrors the legacy autounattend.xml but with -# ONE real local-admin account (no sm-bootstrap) and an embedded preconfig.json that a -# specialize-pass command writes to C:\ProgramData\SilverMetal\preconfig.json. +# ONE real local-admin account (no sm-bootstrap) and an embedded preconfig.json that the +# oobeSystem FirstLogonCommands write (in short base64 chunks) to +# C:\ProgramData\SilverMetal\preconfig.json. The base64 is carried in chunked echo +# commands rather than a single specialize RunSynchronousCommand/Path, because that Path +# is capped at ~259 chars and a full base64 blob overflows it -> "answer file is invalid". function New-SmAnswerFile { param( @@ -29,7 +32,45 @@ function New-SmAnswerFile { function EscContent([string]$s) { $s.Replace('&','&').Replace('<','<').Replace('>','>') } $dn = Esc $DisplayName; $un = Esc $Username; $pw = Esc $Password; $cn = Esc $ComputerName - $writePre = "powershell -NoProfile -ExecutionPolicy Bypass -Command "" New-Item -ItemType Directory -Force 'C:\ProgramData\SilverMetal' | Out-Null; [IO.File]::WriteAllBytes('C:\ProgramData\SilverMetal\preconfig.json', [Convert]::FromBase64String('$preB64')) """ + # Build the oobeSystem FirstLogonCommands. The preconfig base64 is split into short + # (<=150 char) chunks, each appended to a temp file by its own `echo` command, then + # the file is whitespace-stripped + base64-decoded into preconfig.json. This keeps + # every single command line well under the unattend length limits. + $preDir = 'C:\ProgramData\SilverMetal' + $preB64File = "$preDir\pre.b64" + $preFile = "$preDir\preconfig.json" + + # Split base64 into chunks of at most 150 chars (base64 alphabet has no XML/cmd + # metachars, so chunks are safe in `echo` and in XML once `>` is escaped). + $chunkSize = 150 + $chunks = for ($i = 0; $i -lt $preB64.Length; $i += $chunkSize) { + $preB64.Substring($i, [Math]::Min($chunkSize, $preB64.Length - $i)) + } + + $cmds = New-Object System.Collections.Generic.List[string] + # 1. Create the target dir. + $cmds.Add("cmd /c md ""$preDir"" 2>nul") + # 2..N. Append each base64 chunk to the temp file. + foreach ($c in $chunks) { + $cmds.Add("cmd /c >>""$preB64File"" echo $c") + } + # N+1. Strip whitespace (chunks are newline-separated in the file) and decode. + $cmds.Add("powershell -nop -c ""[IO.File]::WriteAllBytes('$preFile',[Convert]::FromBase64String(((gc '$preB64File' -raw) -replace '\s','')))""") + # N+2. Clean up the temp file. + $cmds.Add("cmd /c del ""$preB64File""") + # N+3 (LAST). Launch the SilverMetal toolbox (run-once). + $cmds.Add("cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command ""Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs""") + + $firstLogonSb = New-Object System.Text.StringBuilder + $order = 0 + foreach ($cmd in $cmds) { + $order++ + [void]$firstLogonSb.AppendLine(" ") + [void]$firstLogonSb.AppendLine(" $order") + [void]$firstLogonSb.AppendLine(" $(EscContent $cmd)") + [void]$firstLogonSb.AppendLine(" ") + } + $firstLogonCommands = $firstLogonSb.ToString().TrimEnd() @" @@ -64,17 +105,6 @@ function New-SmAnswerFile { true - - - - - 1 - $(EscContent $writePre) - Write SilverMetal preconfig - - - - $InputLocale$SystemLocale @@ -91,11 +121,7 @@ function New-SmAnswerFile { true1$un$pwtrue</PlainText></Password></AutoLogon> <ComputerName>$cn</ComputerName> <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 SilverMetal toolbox (run-once)</Description> - </SynchronousCommand> +$firstLogonCommands </FirstLogonCommands> <RegisteredOwner>SilverMetal</RegisteredOwner><RegisteredOrganization>SilverLABS</RegisteredOrganization> </component> diff --git a/windows/tests/Collector.Tests.ps1 b/windows/tests/Collector.Tests.ps1 index b4ef7e2..84f4d03 100644 --- a/windows/tests/Collector.Tests.ps1 +++ b/windows/tests/Collector.Tests.ps1 @@ -54,16 +54,19 @@ Describe 'New-SmAnswerFile' { } It 'sets the computer name' { $script:xml | Should -Match '<ComputerName>SILVER-01</ComputerName>' } It 'keeps WillWipeDisk for disk 0' { $script:xml | Should -Match '<WillWipeDisk>true</WillWipeDisk>' } - It 'embeds a base64 preconfig write in specialize' { - $script:xml | Should -Match 'preconfig\.json' - $script:xml | Should -Match 'FromBase64String' - } - It 'embedded preconfig round-trips with the flavour and pin' { - $m = [regex]::Match($script:xml, "FromBase64String\('([^']+)'\)") - $m.Success | Should -BeTrue - $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($m.Groups[1].Value)) | ConvertFrom-Json + It 'preconfig round-trips via chunked FirstLogonCommands' { + # Gather the echo'd base64 chunks in Order, concatenate, strip whitespace, decode. + $chunks = [regex]::Matches($script:xml, 'echo ([A-Za-z0-9+/=]+)') | ForEach-Object { $_.Groups[1].Value } + $b64 = ($chunks -join '') + $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b64)) | ConvertFrom-Json $json.flavour | Should -Be 'developer' $json.bitlocker.pin | Should -Be '246810' } + It 'has no specialize pass anymore' { $script:xml | Should -Not -Match 'pass="specialize"' } + It 'creates the preconfig dir + decodes it at first logon' { + $script:xml | Should -Match 'ProgramData\\SilverMetal' + $script:xml | Should -Match 'FromBase64String' + $script:xml | Should -Match 'preconfig\.json' + } It 'launches the toolbox in FirstLogonCommands' { $script:xml | Should -Match 'SilverOS\.Welcome\.App\.exe' } }