Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Checks

on:
push:
branches: [main]
pull_request:

jobs:
base64-drift:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v4
- name: Verify autounattend base64 ↔ standalone .ps1
run: python3 scripts/check-base64-drift.py
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Contents:
- `scripts/build-qemu.sh` — reproducible local QEMU build, with one rolling screenshot file and a bounded first-boot settle loop
- `scripts/verify.ps1` + `scripts/remediate.ps1` — in-guest verification / remediation loop
- `scripts/firstboot-state.ps1` — lightweight first-boot probe used to wait for concrete SAC runtime components before verification
- `scripts/cocoon-nic-autoheal.ps1` — body of the `CocoonNicAutoHeal` scheduled task; cycles every Net PnP device once a minute to recover chained-clone guests where vm.restore leaves the NIC bound but unable to transmit
- `scripts/install-cocoon-agent-bootstrap.ps1` — downloads the pinned `cocoon-agent` Windows release from GitHub, verifies SHA256, runs the bundled installer; lands at `C:\Scripts\` so `remediate.ps1` can re-invoke it
- `scripts/verify-ch.sh` + `scripts/sac_probe.py` — Cloud Hypervisor runtime validation for DHCP, RDP, real SAC, and clean shutdown
- `.github/workflows/build.yml` — headless QEMU/KVM build on `ubuntu-latest`, publishes to GHCR via ORAS

Expand All @@ -21,6 +23,9 @@ An image produced from this repo is only considered valid for Cocoon if all of t
- It acquires a DHCP lease from a plain `dnsmasq` bridge and reports hostname `COCOON-VM`.
- `3389/tcp` accepts a real RDP authentication attempt.
- `COM1` exposes a real SAC console after Cloud Hypervisor boot. A live `SAC>` prompt is the hard requirement; missing in-guest `ACPI\\PNP0501` enumeration is only a warning. `bcdedit /ems on` by itself is **not** sufficient.
- The `Virtio Vsock STREAM` Winsock provider is registered (Address Family 40 in `netsh winsock show catalog`) and the `viosock` device is bound (`Get-PnpDevice` reports `Status=OK`). Required for `cocoon-agent`'s `AF_VSOCK` listener; `virtio-win-guest-tools.exe /S` does not register the WSP, so a separate `pnputil /add-driver viosock.inf /install` runs at firstboot.
- The `cocoon-agent` Windows service is installed (`Get-Service cocoon-agent`) and running, listening on vsock port 1024. Pinned to a specific `cocoon-agent` release (currently v0.1.1); bumping requires a coordinated update of `scripts/install-cocoon-agent-bootstrap.ps1` + the matching base64 in `autounattend.xml` Order 53.
- The `CocoonNicAutoHeal` scheduled task is registered (1-minute, SYSTEM, HIGHEST) and `C:\CocoonNicAutoHeal.ps1` exists. Recovers chained-clone NDIS state where vm.restore leaves the NIC bound but unable to transmit.
- A remote `shutdown /s /t 10` cleanly terminates the Cloud Hypervisor process.

## Pulling a pre-built image
Expand Down Expand Up @@ -399,7 +404,7 @@ The included [`autounattend.xml`](autounattend.xml) drives the install across th
- **International-Core**: `InputLocale=0409:00000409` only. The component must be present here for Windows 11 25H2 OOBE to skip the country / keyboard selection screens.
- **OOBE**: hides EULA, online account, wireless setup.
- **User account**: local admin `cocoon` with auto-logon (password base64-encoded in XML).
- **FirstLogonCommands**: 53 commands.
- **FirstLogonCommands**: 56 commands.

| Order | Action | Notes |
|--------|------------------------------|-------|
Expand Down Expand Up @@ -429,8 +434,11 @@ The included [`autounattend.xml`](autounattend.xml) drives the install across th
| 47 | **Zero startup delay** | `Explorer\Serialize\StartupDelayInMSec=0` |
| 48-50 | **DWM tuning** | No minimize animation, no drag full windows, ClearType font smoothing |
| 51 | **Disable scheduled tasks** | Compatibility Appraiser, ScheduledDefrag, DiskDiagnostic |
| 52 | **QuickEdit restore** | Restore QuickEdit after install |
| 53 | **Install marker** | `cmd /c "echo %date% %time% > C:\install.success"` |
| 52 | **viosock driver** | `pnputil /add-driver D:\viosock\w11\amd64\viosock.inf /install` (D: + E: + standard + attestation paths). Required for cocoon-agent's `AF_VSOCK` listener — registers the `Virtio Vsock STREAM` Winsock provider (Address Family 40, `viosocklib.dll`). `virtio-win-guest-tools.exe /S` (Order 21) does not install this driver. |
| 53 | **cocoon-agent install** | Download pinned `cocoon-agent_v0.1.1_Windows_x86_64.zip` from GitHub Releases, verify SHA256, run bundled `install-cocoon-agent.ps1` (registers `cocoon-agent` Windows service: LocalSystem, auto-start, restart-on-crash, vsock port 1024). Bootstrap dropped at `C:\Scripts\install-cocoon-agent-bootstrap.ps1` for `remediate.ps1` re-invoke. |
| 54 | **NIC auto-heal task** | Write `C:\CocoonNicAutoHeal.ps1` and register `CocoonNicAutoHeal` schtasks (every minute, SYSTEM, HIGHEST). Cycles all Net PnP devices to recover chained-clone NDIS state. |
| 55 | **QuickEdit restore** | Restore QuickEdit after install |
| 56 | **Install marker** | `cmd /c "echo %date% %time% > C:\install.success"` |

> **Note on WinRM persistence**: `Enable-PSRemoting` + the `AllowUnencrypted`/`Basic` WSMan settings set by orders 14-16 do not always survive the very first post-install reboot on Win11 25H2. `remediate.ps1` re-applies them from the same deterministic settings, and the CI loop reboots → verifies → remediates → re-verifies to make the final image idempotent.

Expand Down
30 changes: 28 additions & 2 deletions autounattend.xml
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,35 @@
<!-- VM performance: disable scheduled tasks that waste CPU -->
<SynchronousCommand wcm:action="add"><Order>51</Order><CommandLine>powershell -Command "Disable-ScheduledTask -TaskName '\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser' -EA SilentlyContinue; Disable-ScheduledTask -TaskName '\Microsoft\Windows\Defrag\ScheduledDefrag' -EA SilentlyContinue; Disable-ScheduledTask -TaskName '\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector' -EA SilentlyContinue"</CommandLine></SynchronousCommand>

<!-- viosock driver (virtio-vsock) install. virtio-win-guest-tools.exe /S
does not register the viosock WSP, so cocoon-agent's AF_VSOCK
listener can't bind. Install directly from the virtio-win ISO.
Both attestation (D:\viosock\w11\amd64) and standard layout, on
D: and E:, mirroring the windowsPE DriverPaths. Verified post-
install: "Virtio Vsock STREAM" Provider Path %SystemRoot%\System32
\viosocklib.dll, Address Family 40 in `netsh winsock show catalog`. -->
<SynchronousCommand wcm:action="add"><Order>52</Order><CommandLine>cmd /c "if exist D:\viosock\w11\amd64\viosock.inf (pnputil /add-driver D:\viosock\w11\amd64\viosock.inf /install) else if exist E:\viosock\w11\amd64\viosock.inf (pnputil /add-driver E:\viosock\w11\amd64\viosock.inf /install) else if exist D:\Win11\amd64\viosock\viosock.inf (pnputil /add-driver D:\Win11\amd64\viosock\viosock.inf /install) else if exist E:\Win11\amd64\viosock\viosock.inf (pnputil /add-driver E:\Win11\amd64\viosock\viosock.inf /install)"</CommandLine></SynchronousCommand>


<!-- cocoon-agent install: download the pinned v0.1.1 release zip from
GitHub, verify SHA256, run the bundled install-cocoon-agent.ps1.
Service registers as cocoon-agent (LocalSystem, auto-start, restart-
on-crash). Bootstrap script lands at C:\Scripts\install-cocoon-agent-
bootstrap.ps1 so remediate.ps1 can re-invoke it. Drift caveat: this
base64 must be regenerated from scripts/install-cocoon-agent-
bootstrap.ps1 whenever the version is bumped. -->
<SynchronousCommand wcm:action="add"><Order>53</Order><CommandLine>powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABCAG8AbwB0AHMAdAByAGEAcAAgAGkAbgBzAHQAYQBsAGwAZQByACAAZgBvAHIAIABjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAA6ACAAZABvAHcAbgBsAG8AYQBkACAAdABoAGUAIABwAGkAbgBuAGUAZAAgAFcAaQBuAGQAbwB3AHMAIAByAGUAbABlAGEAcwBlAAoAIwAgAGYAcgBvAG0AIABHAGkAdABIAHUAYgAsACAAdgBlAHIAaQBmAHkAIABTAEgAQQAyADUANgAsACAAZQB4AHQAcgBhAGMAdAAsACAAcgB1AG4AIAB0AGgAZQAgAGIAdQBuAGQAbABlAGQAIABpAG4AcwB0AGEAbABsAGUAcgAuAAoAIwAKACMAIABUAG8AIABiAHUAbQBwACAAdABoAGUAIABhAGcAZQBuAHQAOgAgAHUAcABkAGEAdABlACAAJAB2AGUAcgBzAGkAbwBuACAAKwAgACQAZQB4AHAAZQBjAHQAZQBkACAAdABvAGcAZQB0AGgAZQByACAAKAByAGUAbABlAGEAcwBlACAAcABhAGcAZQAnAHMACgAjACAAYwBoAGUAYwBrAHMAdQBtAHMALgB0AHgAdAAgAGgAYQBzACAAdABoAGUAIABjAGEAbgBvAG4AaQBjAGEAbAAgAFMASABBADIANQA2ACkALgAgAFQAaABlACAAbQBhAHQAYwBoAGkAbgBnACAAYgBhAHMAZQA2ADQAIABiAGwAbwBjAGsAIABpAG4ACgAjACAAYQB1AHQAbwB1AG4AYQB0AHQAZQBuAGQALgB4AG0AbAAgAE8AcgBkAGUAcgAgADUAMwAgAG0AdQBzAHQAIABiAGUAIAByAGUAZwBlAG4AZQByAGEAdABlAGQAIABmAHIAbwBtACAAdABoAGkAcwAgAGYAaQBsAGUAIABpAG4AIABsAG8AYwBrAHMAdABlAHAACgAjACAAbwByACAAaQBtAGEAZwBlAHMAIAB3AGkAbABsACAAcwBpAGwAZQBuAHQAbAB5ACAAcwBoAGkAcAAgAHQAaABlACAAbwBsAGQAIAB2AGUAcgBzAGkAbwBuAC4ACgAKACQARQByAHIAbwByAEEAYwB0AGkAbwBuAFAAcgBlAGYAZQByAGUAbgBjAGUAIAA9ACAAJwBTAHQAbwBwACcACgAkAFAAcgBvAGcAcgBlAHMAcwBQAHIAZQBmAGUAcgBlAG4AYwBlACAAIAAgACAAPQAgACcAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAnAAoACgAkAHYAZQByAHMAaQBvAG4AIAAgAD0AIAAnADAALgAxAC4AMQAnAAoAJAB1AHIAbAAgACAAIAAgACAAIAA9ACAAIgBoAHQAdABwAHMAOgAvAC8AZwBpAHQAaAB1AGIALgBjAG8AbQAvAGMAbwBjAG8AbwBuAHMAdABhAGMAawAvAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC8AcgBlAGwAZQBhAHMAZQBzAC8AZABvAHcAbgBsAG8AYQBkAC8AdgAkAHYAZQByAHMAaQBvAG4ALwBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAcABlAGMAdABlAGQAIAA9ACAAJwBhAGMAZAAyAGUAOAA3AGMAMwA1ADkANAA3AGQAYgA0ADAANwA2AGYAOQBmAGEAMQBiADkAMQBhADMAMQA2ADIAZgA5ADEAMAA2ADEAZQA1AGEAOAA2ADMAZQA3ADUAZQA1AGIAMAAzADIANwAxADQAZQBmAGQAYwA0ADAAYQBiACcACgAkAHoAaQBwACAAIAAgACAAIAAgAD0AIABKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAdAByAGEAYwB0ACAAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGUAbgB2ADoAVABFAE0AUAAgACIAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQAkAHsAdgBlAHIAcwBpAG8AbgB9ACIACgAKACMAIABEAEgAQwBQAC8ARABOAFMAIABjAGEAbgAgAHQAYQBrAGUAIABhACAAZgBlAHcAIABzAGUAYwBvAG4AZABzACAAdABvACAAcwBlAHQAdABsAGUAIABhAHQAIABmAGkAcgBzAHQAYgBvAG8AdAAuAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgADUAOwAgACQAaQArACsAKQAgAHsACgAgACAAIAAgAHQAcgB5ACAAewAgAEkAbgB2AG8AawBlAC0AVwBlAGIAUgBlAHEAdQBlAHMAdAAgAC0AVQByAGkAIAAkAHUAcgBsACAALQBPAHUAdABGAGkAbABlACAAJAB6AGkAcAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAOwAgAGIAcgBlAGEAawAgAH0ACgAgACAAIAAgAGMAYQB0AGMAaAAgAHsAIABpAGYAIAAoACQAaQAgAC0AZQBxACAANQApACAAewAgAHQAaAByAG8AdwAgAH0AOwAgAFMAdABhAHIAdAAtAFMAbABlAGUAcAAgAC0AUwBlAGMAbwBuAGQAcwAgADUAIAB9AAoAfQAKAAoAJABhAGMAdAB1AGEAbAAgAD0AIAAoAEcAZQB0AC0ARgBpAGwAZQBIAGEAcwBoACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0AQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgApAC4ASABhAHMAaAAuAFQAbwBMAG8AdwBlAHIAKAApAAoAaQBmACAAKAAkAGEAYwB0AHUAYQBsACAALQBuAGUAIAAkAGUAeABwAGUAYwB0AGUAZAApACAAewAKACAAIAAgACAAdABoAHIAbwB3ACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAgAHoAaQBwACAAYwBoAGUAYwBrAHMAdQBtACAAbQBpAHMAbQBhAHQAYwBoADoAIABlAHgAcABlAGMAdABlAGQAIAAkAGUAeABwAGUAYwB0AGUAZAAsACAAZwBvAHQAIAAkAGEAYwB0AHUAYQBsACIACgB9AAoACgBpAGYAIAAoAFQAZQBzAHQALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBSAGUAYwB1AHIAcwBlACAALQBGAG8AcgBjAGUAIAAkAGUAeAB0AHIAYQBjAHQAIAB9AAoARQB4AHAAYQBuAGQALQBBAHIAYwBoAGkAdgBlACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0ARABlAHMAdABpAG4AYQB0AGkAbwBuAFAAYQB0AGgAIAAkAGUAeAB0AHIAYQBjAHQAIAAtAEYAbwByAGMAZQAKAAoAIwAgAEwAbwBjAGEAdABlACAAYgB5ACAAZwBsAG8AYgAgABQgIAB6AGkAcAAgAHAAdQB0AHMAIABlAHgAZQAgAGEAdAAgAHIAbwBvAHQAIABhAG4AZAAgAHAAcwAxACAAdQBuAGQAZQByACAAcABhAGMAawBhAGcAaQBuAGcALwAuAAoAJABwAHMAMQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAJwAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAJABlAHgAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALgBlAHgAZQAnACAAIAAgACAAIAAgACAAIAAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAaQBmACAAKAAtAG4AbwB0ACAAJABwAHMAMQApACAAewAgAHQAaAByAG8AdwAgACIAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAGkAZgAgACgALQBuAG8AdAAgACQAZQB4AGUAKQAgAHsAIAB0AGgAcgBvAHcAIAAiAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AZQB4AGUAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAAoAJgAgACQAcABzADEALgBGAHUAbABsAE4AYQBtAGUAIAAtAEIAaQBuAGEAcgB5AFMAbwB1AHIAYwBlACAAJABlAHgAZQAuAEYAdQBsAGwATgBhAG0AZQAKACcAQAAKAE4AZQB3AC0ASQB0AGUAbQAgAC0ASQB0AGUAbQBUAHkAcABlACAARABpAHIAZQBjAHQAbwByAHkAIAAtAEYAbwByAGMAZQAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwAnACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwBcAGkAbgBzAHQAYQBsAGwALQBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAtAGIAbwBvAHQAcwB0AHIAYQBwAC4AcABzADEAJwAgAC0AVgBhAGwAdQBlACAAJABjAG8AbgB0AGUAbgB0ACAALQBFAG4AYwBvAGQAaQBuAGcAIABBAFMAQwBJAEkAIAAtAEYAbwByAGMAZQAKACYAIAAnAEMAOgBcAFMAYwByAGkAcAB0AHMAXABpAG4AcwB0AGEAbABsAC0AYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQBiAG8AbwB0AHMAdAByAGEAcAAuAHAAcwAxACcACgA=</CommandLine></SynchronousCommand>

<!-- NIC auto-heal: scheduled task that cycles every Net PnP device once a minute.
Recovers chained-clone Win11 guests where vm.restore leaves the NIC bound at
the OS layer but unable to transmit (Status reports 'OK' so a Status='Error'
filter would miss it). Base64-encoded UTF-16LE PowerShell that writes
C:\CocoonNicAutoHeal.ps1 and registers CocoonNicAutoHeal via schtasks. -->
<SynchronousCommand wcm:action="add"><Order>54</Order><CommandLine>powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABjAG8AYwBvAG8AbgAtAG4AaQBjAC0AYQB1AHQAbwBoAGUAYQBsAC4AcABzADEAIAAUICAAYwB5AGMAbABlACAAZQB2AGUAcgB5ACAATgBlAHQALQBjAGwAYQBzAHMAIABQAG4AUAAgAGQAZQB2AGkAYwBlACAAbwBuAGMAZQAuAAoAIwAKACMAIABUAHIAaQBnAGcAZQByAGUAZAAgAGIAeQAgAHQAaABlACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAAcwBjAGgAZQBkAHUAbABlAGQAIAB0AGEAcwBrACAAKAByAGUAZwBpAHMAdABlAHIAZQBkACAAYQB0ACAAZgBpAHIAcwB0AGIAbwBvAHQAKQAKACMAIABvAG4AIABhACAAMQAtAG0AaQBuAHUAdABlACAAcgBlAHAAZQBhAHQALgAgAFIAZQBjAG8AdgBlAHIAcwAgAGMAaABhAGkAbgBlAGQALQBjAGwAbwBuAGUAIABXAGkAbgAxADEAIABnAHUAZQBzAHQAcwAgAHcAaABlAHIAZQAgAHYAbQAuAHIAZQBzAHQAbwByAGUACgAjACAAbABlAGEAdgBlAHMAIAB0AGgAZQAgAE4ASQBDACAAYgBvAHUAbgBkACAAYQB0ACAAdABoAGUAIABPAFMAIABsAGEAeQBlAHIAIABiAHUAdAAgAHUAbgBhAGIAbABlACAAdABvACAAdAByAGEAbgBzAG0AaQB0ACAAFCAgAFMAdABhAHQAdQBzACAAcgBlAHAAbwByAHQAcwAKACMAIAAnAE8ASwAnACAAcwBvACAAYQAgACIAUwB0AGEAdAB1AHMAIAAtAEUAUQAgAEUAcgByAG8AcgAiACAAZgBpAGwAdABlAHIAIAB3AG8AdQBsAGQAIABtAGkAcwBzACAAaQB0AC4AIABDAHkAYwBsAGUAIAB1AG4AYwBvAG4AZABpAHQAaQBvAG4AYQBsAGwAeQAuAAoAJABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAiAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIgAKAGYAbwByAGUAYQBjAGgAIAAoACQAZAAgAGkAbgAgACgARwBlAHQALQBQAG4AcABEAGUAdgBpAGMAZQAgAC0AQwBsAGEAcwBzACAATgBlAHQAKQApACAAewAKACAAIAAgACAARABpAHMAYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAIAAgACAAIABTAHQAYQByAHQALQBTAGwAZQBlAHAAIAAtAFMAZQBjAG8AbgBkAHMAIAAyAAoAIAAgACAAIABFAG4AYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAfQAKACcAQAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgAEMAOgBcAEMAbwBjAG8AbwBuAE4AaQBjAEEAdQB0AG8ASABlAGEAbAAuAHAAcwAxACAALQBWAGEAbAB1AGUAIAAkAGMAbwBuAHQAZQBuAHQAIAAtAEUAbgBjAG8AZABpAG4AZwAgAEEAUwBDAEkASQAgAC0ARgBvAHIAYwBlAAoAcwBjAGgAdABhAHMAawBzACAALwBjAHIAZQBhAHQAZQAgAC8AdABuACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAALwB0AHIAIAAiAHAAbwB3AGUAcgBzAGgAZQBsAGwALgBlAHgAZQAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEUAeABlAGMAdQB0AGkAbwBuAFAAbwBsAGkAYwB5ACAAQgB5AHAAYQBzAHMAIAAtAEYAaQBsAGUAIABDADoAXABDAG8AYwBvAG8AbgBOAGkAYwBBAHUAdABvAEgAZQBhAGwALgBwAHMAMQAiACAALwBzAGMAIABtAGkAbgB1AHQAZQAgAC8AbQBvACAAMQAgAC8AcgB1ACAAUwBZAFMAVABFAE0AIAAvAHIAbAAgAEgASQBHAEgARQBTAFQAIAAvAGYACgA=</CommandLine></SynchronousCommand>

<!-- Restore QuickEdit and mark completion -->
<SynchronousCommand wcm:action="add"><Order>52</Order><CommandLine>reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f</CommandLine></SynchronousCommand>
<SynchronousCommand wcm:action="add"><Order>53</Order><CommandLine>cmd /c "echo %date% %time% > C:\install.success"</CommandLine></SynchronousCommand>
<SynchronousCommand wcm:action="add"><Order>55</Order><CommandLine>reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f</CommandLine></SynchronousCommand>
<SynchronousCommand wcm:action="add"><Order>56</Order><CommandLine>cmd /c "echo %date% %time% > C:\install.success"</CommandLine></SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
Expand Down
69 changes: 69 additions & 0 deletions scripts/check-base64-drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Verify the base64-encoded PowerShell wrappers in autounattend.xml decode
to a here-string body byte-for-byte matching the corresponding standalone
script in scripts/. The base64 in autounattend Orders 53 / 54 is a self-
extracting wrapper that drops the body to disk at firstboot; if the two
sources drift, images silently install the old version.

To regenerate a base64 block after editing the standalone .ps1, run this
script in --regen mode (TODO: not yet implemented; for now, edit by hand
using the recipe below).

Encoding recipe (matches autounattend.xml Order 53/54 format):

wrapper = f'''$content = @'
{ps1_body}
'@
... wrapper body that writes $content to disk and invokes it ...
'''
base64.b64encode(wrapper.encode('utf-16-le')).decode('ascii')

Run from repo root: python3 scripts/check-base64-drift.py
"""

import base64
import re
import sys
from pathlib import Path

PAIRS = [
(53, 'scripts/install-cocoon-agent-bootstrap.ps1'),
(54, 'scripts/cocoon-nic-autoheal.ps1'),
]


def check_pair(autounattend: str, order: int, ps1_path: str) -> tuple[bool, str]:
pattern = (
rf'<Order>{order}</Order><CommandLine>powershell\.exe -NoProfile '
rf'-EncodedCommand ([^<]+)</CommandLine>'
)
m = re.search(pattern, autounattend)
if not m:
return False, f'Order {order}: <Order>...</Order> not found in autounattend.xml'
decoded = base64.b64decode(m.group(1)).decode('utf-16-le')
inner = re.search(r"@'\n(.*?)\n'@", decoded, re.DOTALL)
if not inner:
return False, f'Order {order}: single-quoted here-string body not found in decoded wrapper'
embedded = inner.group(1).rstrip()
standalone = Path(ps1_path).read_text().rstrip()
if embedded == standalone:
return True, f'OK Order {order} matches {ps1_path}'
return False, (
f'Order {order} drift: decoded base64 body does not match {ps1_path}\n'
f' re-encode the wrapper from the standalone .ps1 (see this script header)'
)


def main() -> int:
autounattend = Path('autounattend.xml').read_text()
failures = 0
for order, ps1_path in PAIRS:
ok, msg = check_pair(autounattend, order, ps1_path)
print(msg if ok else f'FAIL: {msg}', file=sys.stderr if not ok else sys.stdout)
if not ok:
failures += 1
return 1 if failures else 0


if __name__ == '__main__':
sys.exit(main())
Loading
Loading