diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..90ce7cf --- /dev/null +++ b/.github/workflows/check.yml @@ -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 diff --git a/README.md b/README.md index 1e3be03..a4ca106 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 | |--------|------------------------------|-------| @@ -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. diff --git a/autounattend.xml b/autounattend.xml index 9a90dd0..5025acb 100644 --- a/autounattend.xml +++ b/autounattend.xml @@ -156,9 +156,35 @@ 51powershell -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" + + 52cmd /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)" + + + + 53powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABCAG8AbwB0AHMAdAByAGEAcAAgAGkAbgBzAHQAYQBsAGwAZQByACAAZgBvAHIAIABjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAA6ACAAZABvAHcAbgBsAG8AYQBkACAAdABoAGUAIABwAGkAbgBuAGUAZAAgAFcAaQBuAGQAbwB3AHMAIAByAGUAbABlAGEAcwBlAAoAIwAgAGYAcgBvAG0AIABHAGkAdABIAHUAYgAsACAAdgBlAHIAaQBmAHkAIABTAEgAQQAyADUANgAsACAAZQB4AHQAcgBhAGMAdAAsACAAcgB1AG4AIAB0AGgAZQAgAGIAdQBuAGQAbABlAGQAIABpAG4AcwB0AGEAbABsAGUAcgAuAAoAIwAKACMAIABUAG8AIABiAHUAbQBwACAAdABoAGUAIABhAGcAZQBuAHQAOgAgAHUAcABkAGEAdABlACAAJAB2AGUAcgBzAGkAbwBuACAAKwAgACQAZQB4AHAAZQBjAHQAZQBkACAAdABvAGcAZQB0AGgAZQByACAAKAByAGUAbABlAGEAcwBlACAAcABhAGcAZQAnAHMACgAjACAAYwBoAGUAYwBrAHMAdQBtAHMALgB0AHgAdAAgAGgAYQBzACAAdABoAGUAIABjAGEAbgBvAG4AaQBjAGEAbAAgAFMASABBADIANQA2ACkALgAgAFQAaABlACAAbQBhAHQAYwBoAGkAbgBnACAAYgBhAHMAZQA2ADQAIABiAGwAbwBjAGsAIABpAG4ACgAjACAAYQB1AHQAbwB1AG4AYQB0AHQAZQBuAGQALgB4AG0AbAAgAE8AcgBkAGUAcgAgADUAMwAgAG0AdQBzAHQAIABiAGUAIAByAGUAZwBlAG4AZQByAGEAdABlAGQAIABmAHIAbwBtACAAdABoAGkAcwAgAGYAaQBsAGUAIABpAG4AIABsAG8AYwBrAHMAdABlAHAACgAjACAAbwByACAAaQBtAGEAZwBlAHMAIAB3AGkAbABsACAAcwBpAGwAZQBuAHQAbAB5ACAAcwBoAGkAcAAgAHQAaABlACAAbwBsAGQAIAB2AGUAcgBzAGkAbwBuAC4ACgAKACQARQByAHIAbwByAEEAYwB0AGkAbwBuAFAAcgBlAGYAZQByAGUAbgBjAGUAIAA9ACAAJwBTAHQAbwBwACcACgAkAFAAcgBvAGcAcgBlAHMAcwBQAHIAZQBmAGUAcgBlAG4AYwBlACAAIAAgACAAPQAgACcAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAnAAoACgAkAHYAZQByAHMAaQBvAG4AIAAgAD0AIAAnADAALgAxAC4AMQAnAAoAJAB1AHIAbAAgACAAIAAgACAAIAA9ACAAIgBoAHQAdABwAHMAOgAvAC8AZwBpAHQAaAB1AGIALgBjAG8AbQAvAGMAbwBjAG8AbwBuAHMAdABhAGMAawAvAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC8AcgBlAGwAZQBhAHMAZQBzAC8AZABvAHcAbgBsAG8AYQBkAC8AdgAkAHYAZQByAHMAaQBvAG4ALwBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAcABlAGMAdABlAGQAIAA9ACAAJwBhAGMAZAAyAGUAOAA3AGMAMwA1ADkANAA3AGQAYgA0ADAANwA2AGYAOQBmAGEAMQBiADkAMQBhADMAMQA2ADIAZgA5ADEAMAA2ADEAZQA1AGEAOAA2ADMAZQA3ADUAZQA1AGIAMAAzADIANwAxADQAZQBmAGQAYwA0ADAAYQBiACcACgAkAHoAaQBwACAAIAAgACAAIAAgAD0AIABKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdABfACQAewB2AGUAcgBzAGkAbwBuAH0AXwBXAGkAbgBkAG8AdwBzAF8AeAA4ADYAXwA2ADQALgB6AGkAcAAiAAoAJABlAHgAdAByAGEAYwB0ACAAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGUAbgB2ADoAVABFAE0AUAAgACIAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQAkAHsAdgBlAHIAcwBpAG8AbgB9ACIACgAKACMAIABEAEgAQwBQAC8ARABOAFMAIABjAGEAbgAgAHQAYQBrAGUAIABhACAAZgBlAHcAIABzAGUAYwBvAG4AZABzACAAdABvACAAcwBlAHQAdABsAGUAIABhAHQAIABmAGkAcgBzAHQAYgBvAG8AdAAuAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgADUAOwAgACQAaQArACsAKQAgAHsACgAgACAAIAAgAHQAcgB5ACAAewAgAEkAbgB2AG8AawBlAC0AVwBlAGIAUgBlAHEAdQBlAHMAdAAgAC0AVQByAGkAIAAkAHUAcgBsACAALQBPAHUAdABGAGkAbABlACAAJAB6AGkAcAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAOwAgAGIAcgBlAGEAawAgAH0ACgAgACAAIAAgAGMAYQB0AGMAaAAgAHsAIABpAGYAIAAoACQAaQAgAC0AZQBxACAANQApACAAewAgAHQAaAByAG8AdwAgAH0AOwAgAFMAdABhAHIAdAAtAFMAbABlAGUAcAAgAC0AUwBlAGMAbwBuAGQAcwAgADUAIAB9AAoAfQAKAAoAJABhAGMAdAB1AGEAbAAgAD0AIAAoAEcAZQB0AC0ARgBpAGwAZQBIAGEAcwBoACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0AQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgApAC4ASABhAHMAaAAuAFQAbwBMAG8AdwBlAHIAKAApAAoAaQBmACAAKAAkAGEAYwB0AHUAYQBsACAALQBuAGUAIAAkAGUAeABwAGUAYwB0AGUAZAApACAAewAKACAAIAAgACAAdABoAHIAbwB3ACAAIgBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAgAHoAaQBwACAAYwBoAGUAYwBrAHMAdQBtACAAbQBpAHMAbQBhAHQAYwBoADoAIABlAHgAcABlAGMAdABlAGQAIAAkAGUAeABwAGUAYwB0AGUAZAAsACAAZwBvAHQAIAAkAGEAYwB0AHUAYQBsACIACgB9AAoACgBpAGYAIAAoAFQAZQBzAHQALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBSAGUAYwB1AHIAcwBlACAALQBGAG8AcgBjAGUAIAAkAGUAeAB0AHIAYQBjAHQAIAB9AAoARQB4AHAAYQBuAGQALQBBAHIAYwBoAGkAdgBlACAALQBQAGEAdABoACAAJAB6AGkAcAAgAC0ARABlAHMAdABpAG4AYQB0AGkAbwBuAFAAYQB0AGgAIAAkAGUAeAB0AHIAYQBjAHQAIAAtAEYAbwByAGMAZQAKAAoAIwAgAEwAbwBjAGEAdABlACAAYgB5ACAAZwBsAG8AYgAgABQgIAB6AGkAcAAgAHAAdQB0AHMAIABlAHgAZQAgAGEAdAAgAHIAbwBvAHQAIABhAG4AZAAgAHAAcwAxACAAdQBuAGQAZQByACAAcABhAGMAawBhAGcAaQBuAGcALwAuAAoAJABwAHMAMQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAJwAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAJABlAHgAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAALQBQAGEAdABoACAAJABlAHgAdAByAGEAYwB0ACAALQBSAGUAYwB1AHIAcwBlACAALQBGAGkAbAB0AGUAcgAgACcAYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALgBlAHgAZQAnACAAIAAgACAAIAAgACAAIAAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxAAoAaQBmACAAKAAtAG4AbwB0ACAAJABwAHMAMQApACAAewAgAHQAaAByAG8AdwAgACIAaQBuAHMAdABhAGwAbAAtAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AcABzADEAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAGkAZgAgACgALQBuAG8AdAAgACQAZQB4AGUAKQAgAHsAIAB0AGgAcgBvAHcAIAAiAGMAbwBjAG8AbwBuAC0AYQBnAGUAbgB0AC4AZQB4AGUAIABuAG8AdAAgAGYAbwB1AG4AZAAgAGkAbgAgACQAZQB4AHQAcgBhAGMAdAAiACAAfQAKAAoAJgAgACQAcABzADEALgBGAHUAbABsAE4AYQBtAGUAIAAtAEIAaQBuAGEAcgB5AFMAbwB1AHIAYwBlACAAJABlAHgAZQAuAEYAdQBsAGwATgBhAG0AZQAKACcAQAAKAE4AZQB3AC0ASQB0AGUAbQAgAC0ASQB0AGUAbQBUAHkAcABlACAARABpAHIAZQBjAHQAbwByAHkAIAAtAEYAbwByAGMAZQAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwAnACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgACcAQwA6AFwAUwBjAHIAaQBwAHQAcwBcAGkAbgBzAHQAYQBsAGwALQBjAG8AYwBvAG8AbgAtAGEAZwBlAG4AdAAtAGIAbwBvAHQAcwB0AHIAYQBwAC4AcABzADEAJwAgAC0AVgBhAGwAdQBlACAAJABjAG8AbgB0AGUAbgB0ACAALQBFAG4AYwBvAGQAaQBuAGcAIABBAFMAQwBJAEkAIAAtAEYAbwByAGMAZQAKACYAIAAnAEMAOgBcAFMAYwByAGkAcAB0AHMAXABpAG4AcwB0AGEAbABsAC0AYwBvAGMAbwBvAG4ALQBhAGcAZQBuAHQALQBiAG8AbwB0AHMAdAByAGEAcAAuAHAAcwAxACcACgA= + + + 54powershell.exe -NoProfile -EncodedCommand JABjAG8AbgB0AGUAbgB0ACAAPQAgAEAAJwAKACMAIABjAG8AYwBvAG8AbgAtAG4AaQBjAC0AYQB1AHQAbwBoAGUAYQBsAC4AcABzADEAIAAUICAAYwB5AGMAbABlACAAZQB2AGUAcgB5ACAATgBlAHQALQBjAGwAYQBzAHMAIABQAG4AUAAgAGQAZQB2AGkAYwBlACAAbwBuAGMAZQAuAAoAIwAKACMAIABUAHIAaQBnAGcAZQByAGUAZAAgAGIAeQAgAHQAaABlACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAAcwBjAGgAZQBkAHUAbABlAGQAIAB0AGEAcwBrACAAKAByAGUAZwBpAHMAdABlAHIAZQBkACAAYQB0ACAAZgBpAHIAcwB0AGIAbwBvAHQAKQAKACMAIABvAG4AIABhACAAMQAtAG0AaQBuAHUAdABlACAAcgBlAHAAZQBhAHQALgAgAFIAZQBjAG8AdgBlAHIAcwAgAGMAaABhAGkAbgBlAGQALQBjAGwAbwBuAGUAIABXAGkAbgAxADEAIABnAHUAZQBzAHQAcwAgAHcAaABlAHIAZQAgAHYAbQAuAHIAZQBzAHQAbwByAGUACgAjACAAbABlAGEAdgBlAHMAIAB0AGgAZQAgAE4ASQBDACAAYgBvAHUAbgBkACAAYQB0ACAAdABoAGUAIABPAFMAIABsAGEAeQBlAHIAIABiAHUAdAAgAHUAbgBhAGIAbABlACAAdABvACAAdAByAGEAbgBzAG0AaQB0ACAAFCAgAFMAdABhAHQAdQBzACAAcgBlAHAAbwByAHQAcwAKACMAIAAnAE8ASwAnACAAcwBvACAAYQAgACIAUwB0AGEAdAB1AHMAIAAtAEUAUQAgAEUAcgByAG8AcgAiACAAZgBpAGwAdABlAHIAIAB3AG8AdQBsAGQAIABtAGkAcwBzACAAaQB0AC4AIABDAHkAYwBsAGUAIAB1AG4AYwBvAG4AZABpAHQAaQBvAG4AYQBsAGwAeQAuAAoAJABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAiAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIgAKAGYAbwByAGUAYQBjAGgAIAAoACQAZAAgAGkAbgAgACgARwBlAHQALQBQAG4AcABEAGUAdgBpAGMAZQAgAC0AQwBsAGEAcwBzACAATgBlAHQAKQApACAAewAKACAAIAAgACAARABpAHMAYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAIAAgACAAIABTAHQAYQByAHQALQBTAGwAZQBlAHAAIAAtAFMAZQBjAG8AbgBkAHMAIAAyAAoAIAAgACAAIABFAG4AYQBiAGwAZQAtAFAAbgBwAEQAZQB2AGkAYwBlACAALQBJAG4AcwB0AGEAbgBjAGUASQBkACAAJABkAC4ASQBuAHMAdABhAG4AYwBlAEkAZAAgAC0AQwBvAG4AZgBpAHIAbQA6ACQAZgBhAGwAcwBlAAoAfQAKACcAQAAKAFMAZQB0AC0AQwBvAG4AdABlAG4AdAAgAC0AUABhAHQAaAAgAEMAOgBcAEMAbwBjAG8AbwBuAE4AaQBjAEEAdQB0AG8ASABlAGEAbAAuAHAAcwAxACAALQBWAGEAbAB1AGUAIAAkAGMAbwBuAHQAZQBuAHQAIAAtAEUAbgBjAG8AZABpAG4AZwAgAEEAUwBDAEkASQAgAC0ARgBvAHIAYwBlAAoAcwBjAGgAdABhAHMAawBzACAALwBjAHIAZQBhAHQAZQAgAC8AdABuACAAQwBvAGMAbwBvAG4ATgBpAGMAQQB1AHQAbwBIAGUAYQBsACAALwB0AHIAIAAiAHAAbwB3AGUAcgBzAGgAZQBsAGwALgBlAHgAZQAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEUAeABlAGMAdQB0AGkAbwBuAFAAbwBsAGkAYwB5ACAAQgB5AHAAYQBzAHMAIAAtAEYAaQBsAGUAIABDADoAXABDAG8AYwBvAG8AbgBOAGkAYwBBAHUAdABvAEgAZQBhAGwALgBwAHMAMQAiACAALwBzAGMAIABtAGkAbgB1AHQAZQAgAC8AbQBvACAAMQAgAC8AcgB1ACAAUwBZAFMAVABFAE0AIAAvAHIAbAAgAEgASQBHAEgARQBTAFQAIAAvAGYACgA= + - 52reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f - 53cmd /c "echo %date% %time% > C:\install.success" + 55reg add "HKCU\Console" /v QuickEdit /t REG_DWORD /d 1 /f + 56cmd /c "echo %date% %time% > C:\install.success" diff --git a/scripts/check-base64-drift.py b/scripts/check-base64-drift.py new file mode 100755 index 0000000..27d33e1 --- /dev/null +++ b/scripts/check-base64-drift.py @@ -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}powershell\.exe -NoProfile ' + rf'-EncodedCommand ([^<]+)' + ) + m = re.search(pattern, autounattend) + if not m: + return False, f'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()) diff --git a/scripts/cocoon-nic-autoheal.ps1 b/scripts/cocoon-nic-autoheal.ps1 new file mode 100644 index 0000000..feaaf87 --- /dev/null +++ b/scripts/cocoon-nic-autoheal.ps1 @@ -0,0 +1,12 @@ +# cocoon-nic-autoheal.ps1 — cycle every Net-class PnP device once. +# +# Triggered by the CocoonNicAutoHeal scheduled task (registered at firstboot) +# on a 1-minute repeat. 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 -EQ Error" filter would miss it. Cycle unconditionally. +$ErrorActionPreference = "SilentlyContinue" +foreach ($d in (Get-PnpDevice -Class Net)) { + Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false + Start-Sleep -Seconds 2 + Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false +} diff --git a/scripts/install-cocoon-agent-bootstrap.ps1 b/scripts/install-cocoon-agent-bootstrap.ps1 new file mode 100644 index 0000000..b1983f0 --- /dev/null +++ b/scripts/install-cocoon-agent-bootstrap.ps1 @@ -0,0 +1,38 @@ +# Bootstrap installer for cocoon-agent: download the pinned Windows release +# from GitHub, verify SHA256, extract, run the bundled installer. +# +# To bump the agent: update $version + $expected together (release page's +# checksums.txt has the canonical SHA256). The matching base64 block in +# autounattend.xml Order 53 must be regenerated from this file in lockstep +# or images will silently ship the old version. + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$version = '0.1.1' +$url = "https://github.com/cocoonstack/cocoon-agent/releases/download/v$version/cocoon-agent_${version}_Windows_x86_64.zip" +$expected = 'acd2e87c35947db4076f9fa1b91a3162f91061e5a863e75e5b032714efdc40ab' +$zip = Join-Path $env:TEMP "cocoon-agent_${version}_Windows_x86_64.zip" +$extract = Join-Path $env:TEMP "cocoon-agent-${version}" + +# DHCP/DNS can take a few seconds to settle at firstboot. +for ($i = 1; $i -le 5; $i++) { + try { Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing; break } + catch { if ($i -eq 5) { throw }; Start-Sleep -Seconds 5 } +} + +$actual = (Get-FileHash -Path $zip -Algorithm SHA256).Hash.ToLower() +if ($actual -ne $expected) { + throw "cocoon-agent zip checksum mismatch: expected $expected, got $actual" +} + +if (Test-Path $extract) { Remove-Item -Recurse -Force $extract } +Expand-Archive -Path $zip -DestinationPath $extract -Force + +# Locate by glob — zip puts exe at root and ps1 under packaging/. +$ps1 = Get-ChildItem -Path $extract -Recurse -Filter 'install-cocoon-agent.ps1' | Select-Object -First 1 +$exe = Get-ChildItem -Path $extract -Recurse -Filter 'cocoon-agent.exe' | Select-Object -First 1 +if (-not $ps1) { throw "install-cocoon-agent.ps1 not found in $extract" } +if (-not $exe) { throw "cocoon-agent.exe not found in $extract" } + +& $ps1.FullName -BinarySource $exe.FullName diff --git a/scripts/remediate.ps1 b/scripts/remediate.ps1 index 5004769..0af4f26 100644 --- a/scripts/remediate.ps1 +++ b/scripts/remediate.ps1 @@ -148,4 +148,45 @@ Disable-ScheduledTask -TaskName '\Microsoft\Windows\Application Experience\Micro Disable-ScheduledTask -TaskName '\Microsoft\Windows\Defrag\ScheduledDefrag' -ErrorAction SilentlyContinue Disable-ScheduledTask -TaskName '\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector' -ErrorAction SilentlyContinue +# --- viosock driver --- +$vsockCat = (netsh winsock show catalog 2>&1 | Out-String) +if ($vsockCat -notmatch 'Virtio Vsock STREAM') { + Write-Output "Installing viosock driver..." + foreach ($p in 'D:\viosock\w11\amd64\viosock.inf', + 'E:\viosock\w11\amd64\viosock.inf', + 'D:\Win11\amd64\viosock\viosock.inf', + 'E:\Win11\amd64\viosock\viosock.inf') { + if (Test-Path $p) { pnputil /add-driver $p /install | Out-Null; break } + } +} + +# --- NIC auto-heal task --- +$autoheal = schtasks /query /tn CocoonNicAutoHeal 2>&1 | Out-String +if ($LASTEXITCODE -ne 0) { + Write-Output "Re-creating CocoonNicAutoHeal task..." + @' +$ErrorActionPreference = "SilentlyContinue" +foreach ($d in (Get-PnpDevice -Class Net)) { + Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false + Start-Sleep -Seconds 2 + Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false +} +'@ | Out-File -Encoding ASCII -FilePath 'C:\CocoonNicAutoHeal.ps1' -Force + schtasks /create /tn CocoonNicAutoHeal ` + /tr 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\CocoonNicAutoHeal.ps1' ` + /sc minute /mo 1 /ru SYSTEM /rl HIGHEST /f | Out-Null +} + +# --- cocoon-agent service --- +$cocoon = Get-Service -Name cocoon-agent -ErrorAction SilentlyContinue +if ($null -eq $cocoon -or $cocoon.Status -ne 'Running') { + $bootstrap = 'C:\Scripts\install-cocoon-agent-bootstrap.ps1' + if (Test-Path $bootstrap) { + Write-Output "Re-running cocoon-agent bootstrap..." + & $bootstrap + } else { + Write-Output "WARN: cocoon-agent bootstrap missing at $bootstrap; service install requires re-running autounattend" + } +} + Write-Output "=== Remediation complete ===" diff --git a/scripts/verify.ps1 b/scripts/verify.ps1 index 6d41624..b1a546a 100644 --- a/scripts/verify.ps1 +++ b/scripts/verify.ps1 @@ -183,6 +183,32 @@ $vgt = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninsta Where-Object { $_.DisplayName -match 'Virtio-win' } Check "virtio-win guest tools installed" ($null -ne $vgt) +# --- viosock (virtio-vsock) driver: required for cocoon-agent's AF_VSOCK --- +# virtio-win-guest-tools.exe /S installs viostor / NetKvm / balloon but skips +# viosock; autounattend Order 52 runs pnputil for it. Two checks: hardware bind +# (Get-PnpDevice) plus Winsock provider registration (catalog must list AF=40). +$vsockDev = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | + Where-Object { $_.InstanceId -like '*VEN_1AF4*DEV_1053*' } +Check "viosock device bound (Status=OK)" ($null -ne $vsockDev -and $vsockDev.Status -eq 'OK') + +$vsockCat = (netsh winsock show catalog 2>&1 | Out-String) +Check "Virtio Vsock STREAM provider registered" ($vsockCat -match 'Virtio Vsock STREAM') + +# --- NIC auto-heal scheduled task --- +$autoheal = schtasks /query /tn CocoonNicAutoHeal /fo LIST 2>&1 | Out-String +Check "CocoonNicAutoHeal task registered" ($LASTEXITCODE -eq 0 -and $autoheal -match 'CocoonNicAutoHeal') +Check "C:\CocoonNicAutoHeal.ps1 present" (Test-Path 'C:\CocoonNicAutoHeal.ps1') + +# --- cocoon-agent service --- +$cocoon = Get-Service -Name cocoon-agent -ErrorAction SilentlyContinue +Check "cocoon-agent service running" ($null -ne $cocoon -and $cocoon.Status -eq 'Running') +Check "cocoon-agent bootstrap present" (Test-Path 'C:\Scripts\install-cocoon-agent-bootstrap.ps1') +$cocoonExe = "$env:ProgramFiles\Cocoon\cocoon-agent.exe" +if (Test-Path $cocoonExe) { + $ver = (& $cocoonExe --version 2>&1 | Out-String).Trim() + Write-Host " cocoon-agent: $ver" +} + # --- Install marker --- Check "C:\install.success exists" (Test-Path C:\install.success)