5 Commits

Author SHA1 Message Date
Simone 41611d46a7 fix(template): §7.4 Validate-SetupState -Remediate flag + Setup/Deploy tweaks 2026-05-10 14:26:35 +02:00
Simone 7d6ae42fcf fix(template): §7.4 e2e fixes + autologin + validation scripts
Fix emerged during §7.4 e2e test run (2026-05-10):
- Deploy: set UsoSvc to Manual explicitly (Server 2025 default = Automatic)
- Setup cleanup: re-apply Set-Service Disabled on wuauserv/UsoSvc after DISM
  StartComponentCleanup (WaaSMedicSvc resets StartType during component store cleanup)
- Deploy + Setup: add Administrator autologin (AutoAdminLogon, DefaultUserName,
  DefaultPassword, DefaultDomainName) in post-install.ps1; Assert-Step 5c validates it
- Add Validate-DeployState.ps1: standalone host-side check of all Deploy-set state
- Add Validate-SetupState.ps1: standalone host-side check of full post-Setup state
- Mark §7.4, §7.1 and §7.2 e2e validation items as complete in TODO.md
- Update docs: WINDOWS-TEMPLATE-SETUP.md (arch table, validation scripts section,
  autologin in confine Deploy/Setup); TEST-7.4-e2e.md checklist marked done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 11:30:11 +02:00
Simone 57e4a9713e docs: add §7.4 e2e test runbook for Deploy↔Setup refactor validation
Step-by-step guide to validate §7 refactor on a real VM:
Deploy stand-alone, state verification, Prepare with/without -SkipWindowsUpdate,
snapshot promotion. Includes expected values table and checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 07:19:27 +02:00
Simone ad39aa5cf4 docs(todo): §7 — replace emoji with [x]/[ ] checkboxes, reformat items
Convert §7.1/7.2/7.3 from emoji  + prose into standard [x] checkbox lists.
§7.4 items converted to [ ] open checkboxes. Consistent with rest of TODO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 07:11:03 +02:00
Simone 260e275b89 refactor(template): §7 — move OS hardening to Deploy, Setup becomes validation-only
§7.1 — Setup Steps 1/3/4b/5b/5c converted to Assert-Step only:
  - Deploy post-install.ps1 now owns: Firewall (all profiles disabled),
    WinRM MaxMemory=2048MB/IdleTimeout=2h/MaxProcesses=25/StartupType=Automatic,
    lock screen (NoLockScreen), SM GPO policy key + scheduled task disable,
    DisableCAD in Winlogon, OOBE non-policy key.
  - Setup no longer re-applies these — validates them, throws if Deploy missed any.

§7.2 — WU lifecycle Strategy B:
  - Deploy: replaces sc.exe disable with GPO locks only (NoAutoUpdate=1,
    DisableWindowsUpdateAccess=1); services remain Manual (Windows default).
  - Setup Step 6 Step A already clears these locks before COM API; Step 6b
    permanently disables services post-update. No Setup changes needed.

§7.3 — Docs updated:
  - Deploy .DESCRIPTION reflects full hardening ownership.
  - Setup .DESCRIPTION marks validation-only steps, updates step ordering rationale.
  - WINDOWS-TEMPLATE-SETUP.md: three-script architecture table, CI-WinBuild.vmx
    path, Fase C step list + rationale + validation table updated.

TODO: §7.1/7.2/7.3 marked done. §7.4 e2e pending (requires new VM snapshot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 01:27:29 +02:00
8 changed files with 1242 additions and 398 deletions
+47 -81
View File
@@ -570,18 +570,18 @@ build più ampi senza toccare il template.
Tool da aggiungere come step `Setup-WinBuild2025` (ognuno con `Assert-Step`): Tool da aggiungere come step `Setup-WinBuild2025` (ognuno con `Assert-Step`):
| Tool | Versione | Razionale | | Tool | Versione | Razionale |
| ------------------- | ----------- | ------------------------------------------------------------ | | ------------------- | ----------------------- | -------------------------------------------------------------- |
| **Git for Windows** | latest LTS | Self-clone in VM (alt a host-zip-transfer), submodule fetch | | **Git for Windows** | latest LTS | Self-clone in VM (alt a host-zip-transfer), submodule fetch |
| **7-Zip** | latest | Universale unpack/zip; molti installer e workflow ne dipendono | | **7-Zip** | latest | Universale unpack/zip; molti installer e workflow ne dipendono |
| **PowerShell 7.x** | latest LTS | Più veloce di 5.1, parallel pipelines, operatori moderni | | **PowerShell 7.x** | latest LTS | Più veloce di 5.1, parallel pipelines, operatori moderni |
| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) | | **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) |
| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi | | **CMake** | latest | Build system cross-platform per progetti C/C++ nativi |
| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) | | **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) |
| **WiX Toolset** | v4 stable | MSI building da .NET projects | | **WiX Toolset** | v4 stable | MSI building da .NET projects |
| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR | | **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR |
| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky | | **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky |
| **vcpkg** | latest | Package manager C++ per dipendenze native | | **vcpkg** | latest | Package manager C++ per dipendenze native |
**Approccio**: **Approccio**:
- Pinning hash SHA256 per ogni installer (vedi §1.3) - Pinning hash SHA256 per ogni installer (vedi §1.3)
@@ -623,84 +623,50 @@ OOBE telemetry, WU services disable — entrambi gli script li toccano.
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1), File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
**Da MUOVERE Setup → Deploy**: - [x] Aggiungere `Set-NetFirewallProfile -Enabled False` (tutti i profili) in Deploy post-install.ps1
- [x] Aggiungere `MaxMemoryPerShellMB=2048`, `IdleTimeOut=7200000`, `MaxProcessesPerShell=25`, `StartupType=Automatic` in Deploy dopo Enable-PSRemoting
| Concern Setup | Step attuale Setup | Azione Deploy | - [x] Aggiungere in Deploy items mancanti rispetto a Setup 5c: lock screen (`NoLockScreen`), SM GPO policy key (`DoNotOpenAtLogon`), SM scheduled task disable, `DisableCAD` in Winlogon, OOBE non-policy key
| ----------------------------- | ------------------ | ------------------------------------------- | - [x] Setup Step 1 (Firewall) → Assert-Step only
| Firewall off all profiles | Step 1 | Aggiungere `Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False` in post-install (ora Deploy aggiunge solo regole RDP/ICMP) | - [x] Setup Step 2 (Defender) → già validation-only (refactor 2026-05-09)
| WinRM tuning memoria/timeout | Step 3 (parziale) | Aggiungere `MaxMemoryPerShellMB=2048`, `IdleTimeOut=7200000`, `MaxProcessesPerShell=25` in post-install dopo Enable-PSRemoting | - [x] Setup Step 3 (WinRM tuning) → Assert-Step only (rimuovi Enable-PSRemoting, winrm set, Set-Item, Set-Service)
- [x] Setup Step 4b (UAC) → Assert-Step only
**Da CONVERTIRE in Setup a Assert-Step (validation-only)**: - [x] Setup Step 5b (Explorer LaunchTo) → Assert-Step only
- [x] Setup Step 5c (Server Manager / DisableCAD / OOBE) → Assert-Step only
| Step Setup | Diventa | - [x] E2e validation — completata §7.4 (2026-05-10)
| ---------- | ------- |
| Step 1 Firewall | `Assert-Step` 3 profili Enabled=False (richiede prima la riga "Da MUOVERE" → Deploy: Set-NetFirewallProfile -Enabled False) |
| Step 2 Defender | Già validation-only (refactor 2026-05-09) |
| Step 3 WinRM tuning | `Assert-Step` MaxMemoryPerShellMB, IdleTimeOut, AllowUnencrypted, Basic |
| Step 4b UAC | `Assert-Step` EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| Step 5b Explorer LaunchTo | `Assert-Step` HKCU LaunchTo=1 |
| Step 5c Server Manager / DisableCAD / OOBE | `Assert-Step` HKLM machine policy + DisableCAD=1 + OOBE policy |
**Da MANTENERE in Setup (build env, non base OS)**:
- Step 4: build user `ci_build` + admin
- Step 5: `C:\CI\{build,output,scripts}` dirs
- Step 7-9: .NET SDK, Python, VS Build Tools
- Step 10: Toolchain cross-check
- Cleanup, Final pre-snapshot gate
**Test acceptance**:
- Deploy stand-alone produce VM con tutte le validation Setup che passano (no Setup run)
- Setup run dopo Deploy: tutte Assert-Step pass `[OK]` immediato (no work)
- Test `--Skip` orthogonali (no regressione)
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle) ### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle)
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1), File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
**Stato attuale (conflitto ordering)**: Strategia B: Deploy lascia servizi WU `start=demand` (Manual, default Windows) e scrive solo
- Deploy: `sc.exe config wuauserv start= disabled` + `sc.exe stop wuauserv` (idem UsoSvc, WaaSMedicSvc) + GPO `NoAutoUpdate=1` GPO `NoAutoUpdate=1` + `DisableWindowsUpdateAccess=1`. Setup Step 6 clears i GPO locks (Step A),
- Setup Step 6: necessita servizi UP per SchTask SYSTEM run WU avvia WU via SchTask SYSTEM, poi Step 6b disabilita servizi permanentemente post-update.
- Setup Step 6b: re-disabilita servizi → doppio lavoro, fragile
**Strategia B — separazione pulita**: - [x] Deploy: rimuovere `sc.exe config wuauserv start= disabled` (e UsoSvc, WaaSMedicSvc)
- [x] Deploy: aggiungere GPO `DisableWindowsUpdateAccess=1` (era mancante; `NoAutoUpdate=1` già presente)
**Deploy** (post-install.ps1): - [x] Setup Step 6 Step A: già rimuove `DisableWindowsUpdateAccess` + `NoAutoUpdate` prima del COM API — nessuna modifica necessaria
- Lascia servizi WU `start=demand` (Manual, default Windows) — NON `disabled`, NON `stop` - [x] Setup Step 6 Step D: già fa `Set-Service start=Manual` + `Start-Service` — idempotente con servizi già Manual
- Setta solo GPO `NoAutoUpdate=1`, `DisableWindowsUpdateAccess=1` → no background scheduled WU - [x] Setup Step 6b: già disabilita wuauserv/UsoSvc + GPO keys — nessuna modifica necessaria
- Risultato: WU è invocabile on-demand ma non parte mai da solo - [x] E2e validation — completata §7.4 (2026-05-10)
**Setup** (Step 6 / 6b):
- Step 6 (skip se `-SkipWindowsUpdate`): `sc.exe config wuauserv start= demand` (idempotent), avvia SchTask SYSTEM, attende exit code, parse 0/2/3/3010
- Step 6b (sempre, anche con `-Skip`): `sc.exe config wuauserv start= disabled` + `sc.exe stop wuauserv` (idem UsoSvc, WaaSMedicSvc), conferma GPO presenti — snapshot capture WU permanently off
**Pattern run periodico patches** (allineato §2.5 snapshot versionato):
- Re-deploy template `BaseClean_<yyyyMMdd>` ogni N mesi via Deploy + Prepare (no `-Skip`) → patch applicate al template fresh
**Validazione finale** (Setup Final gate):
- `wuauserv.StartType == Disabled`
- `UsoSvc.StartType == Disabled`
- GPO `NoAutoUpdate=1`, `DisableWindowsUpdateAccess=1`
**Test acceptance**:
- Deploy stand-alone: `Get-Service wuauserv | Select StartType``Manual`, `Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name NoAutoUpdate` → 1
- Setup `-SkipWindowsUpdate`: Final gate vede `StartType=Disabled` (impostato da Step 6b)
- Setup senza `-Skip`: WU runs, exit 0/2/3, poi servizi disabled
### 7.3 [P2] Update header docs di entrambi gli script ### 7.3 [P2] Update header docs di entrambi gli script
- [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1) `.DESCRIPTION`:
enumerare tutte le hardening voci che Deploy ora possiede (firewall, WinRM tuning,
WU GPO non disable)
- [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1) `.DESCRIPTION`:
marcare Step 1/3/4b/5b/5c come "validation-only — set by Deploy"
- [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): aggiornare descrizione
"Cosa fa Setup-WinBuild2025" — riflettere nuovo confine
### 7.4 [P2] Test e2e refactor - [x] [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1) `.DESCRIPTION`: aggiornato con tutte le hardening voci (firewall, WinRM tuning, WU GPO strategy B, lock screen, SM, OOBE)
- Run Deploy stand-alone su VM test → verifica tutte le hardening voci presenti - [x] [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1) `.DESCRIPTION`: Steps 1/3/4b/5b/5c marcati "validation-only — set by Deploy"; step ordering rationale aggiornato
(`reg query` per chiavi UAC/OOBE/Server Manager, `Get-NetFirewallProfile`, `Get-Service WMUauserv`) - [x] [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): architettura "tre script", VMX path `CI-WinBuild.vmx`, Fase C step list + razionale + tabella validazioni aggiornati
- Run Prepare-WinBuild2025 dopo Deploy → tutti `Assert-Step` pass `[OK]` senza work
- Run Prepare con `-SkipWindowsUpdate` → comportamento invariato ### 7.4 [P2] Test e2e refactor — COMPLETATO 2026-05-10
- Run Prepare senza `-Skip` → WU runs, services lifecycled correttamente
- [x] Deploy stand-alone su VM test → `Get-NetFirewallProfile` tutti `Enabled=False`, `Get-Service wuauserv | Select StartType``Manual`, `NoAutoUpdate=1` GPO presente
- [x] Prepare dopo Deploy con `-SkipWindowsUpdate` → tutti `Assert-Step` passano `[OK]` senza eseguire Set (log mostra solo validation output)
- [x] Prepare senza `-SkipWindowsUpdate` → WU (0 update trovati, ResultCode=0), Step 6b disabilita wuauserv/UsoSvc (`StartType=Disabled`)
- [x] Final gate del guest passa su tutti e 7 i check; snapshot `BaseClean` preso
**Fix emersi durante §7.4** (applicati):
- Deploy: `UsoSvc` impostato a `Manual` esplicitamente (default Server 2025 = Automatic)
- Setup cleanup: `Set-Service -StartupType Disabled` ri-applicato su wuauserv/UsoSvc dopo DISM `StartComponentCleanup` (DISM/WaaSMedicSvc può resettare StartType)
- Deploy + Setup: autologin Administrator (`AutoAdminLogon=1`) aggiunto in post-install.ps1 e validato in Assert-Step 5c
- Nuovi script: `template/Validate-DeployState.ps1`, `template/Validate-SetupState.ps1`
### 7.5 [P3] Rinomina Setup → Setup-CITools (opzionale, futuro) ### 7.5 [P3] Rinomina Setup → Setup-CITools (opzionale, futuro)
Una volta che Setup fa solo build customization, nome `Setup-WinBuild2025` è impreciso — Una volta che Setup fa solo build customization, nome `Setup-WinBuild2025` è impreciso —
+169
View File
@@ -0,0 +1,169 @@
# §7.4 — E2e test refactor Deploy↔Setup
Valida che il refactor §7 (2026-05-10) funzioni su VM reale:
Deploy possiede OS hardening → Setup valida senza re-applicare.
**Non toccare l'existing template** (`CI-WinBuild.vmx` + snapshot `BaseClean`).
Usare VM separata per i test.
---
## Prerequisiti
- `F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso` presente
- `F:\CI\ISO\VMware-tools-windows.iso` presente
- VMware Workstation aperto, sufficiente spazio su `F:\CI\Templates\WinBuildTest\`
---
## Step 1 — Deploy su VM test
```powershell
cd N:\Code\Workspace\Local-CI-CD-System\template
.\Deploy-WinBuild2025.ps1 `
-WinISO 'F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso' `
-ToolsISO 'F:\CI\ISO\VMware-tools-windows.iso' `
-VMXPath 'F:\CI\Templates\WinBuildTest\CI-WinBuild-test.vmx' `
-VMName 'WinBuild2025-test' `
-ProductKey 'TVRH6-WHNXV-R9WG3-9XRFY-MY832'
```
Durata attesa: 3090 min. Exit 0 = OK, snapshot `PostInstall` preso automaticamente.
Se timeout su `install_complete.flag`: aumentare `-GuestPollMaxMinutes 120`.
---
## Step 2 — Verifica stato Deploy (§7.1 + §7.2)
Sostituire `192.168.79.xxx` con IP reale della VM test (`ipconfig` dentro la VM).
```powershell
$ip = '192.168.79.xxx'
$cred = Get-Credential # Administrator / WinBuild!
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
Invoke-Command -ComputerName $ip -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
"=== §7.1 Firewall ==="
Get-NetFirewallProfile | Select-Object Name, Enabled
"=== §7.2 WU services ==="
Get-Service wuauserv, UsoSvc | Select-Object Name, StartType, Status
"=== §7.2 WU GPO ==="
$au = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU'
$wu = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
[PSCustomObject]@{
NoAutoUpdate = (Get-ItemProperty $au -EA SilentlyContinue).NoAutoUpdate
DisableWindowsUpdateAccess = (Get-ItemProperty $wu -EA SilentlyContinue).DisableWindowsUpdateAccess
}
"=== §7.1 WinRM limits ==="
[PSCustomObject]@{
MaxMemoryPerShellMB = (Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB).Value
IdleTimeOut = (Get-Item WSMan:\localhost\Shell\IdleTimeOut).Value
}
"=== §7.1 UAC ==="
Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' |
Select-Object EnableLUA, LocalAccountTokenFilterPolicy
}
```
**Atteso**:
| Check | Valore atteso |
|---|---|
| Firewall tutti profili | `Enabled=False` |
| `wuauserv` StartType | `Manual` |
| `UsoSvc` StartType | `Manual` |
| `NoAutoUpdate` | `1` |
| `DisableWindowsUpdateAccess` | `1` |
| `MaxMemoryPerShellMB` | `>= 2048` |
| `EnableLUA` | `0` |
---
## Step 3 — Prepare -SkipWindowsUpdate (Assert-Step validation)
```powershell
.\Prepare-WinBuild2025.ps1 `
-VMIPAddress 192.168.79.xxx `
-SkipWindowsUpdate
```
**Cosa verificare nel log**:
- Step 1 (Firewall): solo `[OK]` — nessun `Set-NetFirewallProfile`
- Step 3 (WinRM): solo `[OK]` — nessun `Enable-PSRemoting``winrm set`
- Step 4b (UAC): solo `[OK]` — nessun `Set-ItemProperty EnableLUA`
- Step 5b (Explorer): solo `[OK]`
- Step 5c (CIUX): solo `[OK]`
- Step 4 (user `ci_build`): crea utente — operativo, non validation
- Step 5 (CI dirs): crea `C:\CI\{build,output,scripts}` — operativo
- Step 6: `[Setup] Skipping Windows Update` — corretto con `-SkipWindowsUpdate`
- Step 6b: disabilita wuauserv/UsoSvc — deve girare sempre
- Step 7-10: .NET, Python, VS Build Tools installati
- Final gate: tutti e 7 i check `[OK]`
- Exit code: `0`
Se un Assert-Step fallisce con `[FAIL]` su Step 1/3/4b/5b/5c → Deploy non ha applicato
quel setting → debug Deploy post-install.ps1 (controllare log nella VM).
---
## Step 4 — Prepare senza -Skip (WU lifecycle §7.2)
> Solo se Step 3 è passato. La VM deve avere internet (VMnet8 NAT).
```powershell
.\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.xxx
```
**Atteso**:
- Step 6 Step A: rimuove `DisableWindowsUpdateAccess` e `NoAutoUpdate` GPO keys
- Step 6 Step D: avvia wuauserv/UsoSvc/bits/cryptsvc (già Manual → solo Start)
- Step 6: WU gira via SchTask SYSTEM, ResultCode ∈ {0, 2, 3}
- Step 6b: `wuauserv StartType=Disabled`, `UsoSvc StartType=Disabled`, GPO keys re-scritti
- Final gate `Windows Update permanently disabled``[OK]`
- Exit code: `0` (o `3010` se WU richiede reboot — ripetere con `-SkipWindowsUpdate`)
---
## Step 5 — Snapshot e promozione (opzionale)
Se tutti i check di Step 3 e 4 passano:
```powershell
# Spegni VM test
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
-T ws stop 'F:\CI\Templates\WinBuildTest\CI-WinBuild-test.vmx' soft
# Snapshot versionato
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
-T ws snapshot 'F:\CI\Templates\WinBuildTest\CI-WinBuild-test.vmx' `
"BaseClean_$(Get-Date -Format yyyyMMdd)"
```
Per promuovere a produzione (rimpiazza template esistente):
1. Copia `F:\CI\Templates\WinBuildTest\``F:\CI\Templates\WinBuild\` (backup prima)
2. Aggiorna `GITEA_CI_TEMPLATE_PATH` in `runner/config.yaml` se path cambia
3. Aggiorna `GITEA_CI_SNAPSHOT_NAME` se usi nome versionato (vedi TODO §2.5)
---
## Checklist §7.4 — COMPLETATA 2026-05-10
- [x] Step 1: Deploy exit 0, snapshot `PostInstall` presente
- [x] Step 2: Firewall=False, wuauserv=Manual, NoAutoUpdate=1, MaxMemory≥2048, UAC=0
(UsoSvc era Automatic — fix applicato in Deploy; corretto manualmente sulla VM test)
- [x] Step 3: Prepare `-SkipWindowsUpdate` exit 0, nessun Set in Step 1/3/4b/5b/5c
- [x] Step 4: Prepare senza `-Skip` exit 0 (WU: 0 update, ResultCode=0), wuauserv=Disabled post-Step 6b
- [x] Snapshot `BaseClean` preso con successo
**Fix applicati durante il test**:
- Deploy: `UsoSvc` esplicitamente a `Manual` (era omesso, default Server 2025 = Automatic)
- Setup cleanup: ri-applica `Disabled` su wuauserv/UsoSvc dopo DISM (WaaSMedicSvc resets StartType)
- Deploy + Setup 5c: autologin Administrator (`AutoAdminLogon=1`, `DefaultUserName`, `DefaultPassword`, `DefaultDomainName`)
- Nuovi: `Validate-DeployState.ps1`, `Validate-SetupState.ps1`
+80 -48
View File
@@ -9,12 +9,19 @@ credenziali archiviate, ISO Windows Server presente).
--- ---
## Architettura: due script ## Architettura: tre script
| Script | Dove gira | Scopo | | Script | Dove gira | Scopo |
| ---------------------------------------- | --------------- | -------------------------------------------------- | | ---------------------------------------- | --------------- | ------------------------------------------------------------------- |
| `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue lo script in VM | | `template/Deploy-WinBuild2025.ps1` | **Host** | Opzionale: crea VM e installa Windows unattended da ISO |
| `template/Setup-WinBuild2025.ps1` | **Dentro VM** | Provisioning effettivo (firewall, defender, tool) | | `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue Setup nel guest via WinRM |
| `template/Setup-WinBuild2025.ps1` | **Dentro VM** | CI toolchain (user, .NET, Python, VS) + validazione hardening Deploy |
| `template/Validate-DeployState.ps1` | **Host** | Valida stato post-Deploy prima di eseguire Prepare |
| `template/Validate-SetupState.ps1` | **Host** | Valida stato post-Setup (Deploy + Setup) prima dello snapshot |
**Confine Deploy / Setup** (refactor §7, 2026-05-10):
- `Deploy-WinBuild2025.ps1` si occupa dell'**OS hardening** (Firewall, WinRM, UAC, Defender, UX tweaks, autologin Administrator, WU GPO locks). Se usato, Setup valida questi come Assert-Step senza re-applicarli.
- `Setup-WinBuild2025.ps1` si occupa della **CI customization** (utente `ci_build`, cartelle `C:\CI`, toolchain .NET/Python/VS, Windows Update lifecycle, cleanup, final gate).
`Prepare-WinBuild2025.ps1` apre una WinRM session, copia `Setup-WinBuild2025.ps1` in `Prepare-WinBuild2025.ps1` apre una WinRM session, copia `Setup-WinBuild2025.ps1` in
`C:\CI\`, lo esegue, raccoglie l'exit code, valida lo stato del guest. `C:\CI\`, lo esegue, raccoglie l'exit code, valida lo stato del guest.
@@ -30,7 +37,7 @@ credenziali archiviate, ISO Windows Server presente).
| RAM | 6144 MB (6 GB) | | RAM | 6144 MB (6 GB) |
| Disco | 80 GB thin provisioned | | Disco | 80 GB thin provisioned |
| NIC | **VMnet8 (NAT)** — internet richiesto per WU + installer | | NIC | **VMnet8 (NAT)** — internet richiesto per WU + installer |
| VMX path | `F:\CI\Templates\WinBuild\WinBuild.vmx` | | VMX path | `F:\CI\Templates\WinBuild\CI-WinBuild.vmx` |
| ISO | Windows Server 2025 (da `F:\CI\ISO\`) | | ISO | Windows Server 2025 (da `F:\CI\ISO\`) |
--- ---
@@ -41,7 +48,7 @@ credenziali archiviate, ISO Windows Server presente).
- File → New Virtual Machine → Custom - File → New Virtual Machine → Custom
- Hardware: 4 vCPU, 6144 MB, 80 GB thin - Hardware: 4 vCPU, 6144 MB, 80 GB thin
- Network: **VMnet8 (NAT)** - Network: **VMnet8 (NAT)**
- VMX: `F:\CI\Templates\WinBuild\WinBuild.vmx` - VMX: `F:\CI\Templates\WinBuild\CI-WinBuild.vmx`
- CD/DVD: monta ISO Windows Server 2025 - CD/DVD: monta ISO Windows Server 2025
### A.2 Installa Windows Server 2025 ### A.2 Installa Windows Server 2025
@@ -142,21 +149,22 @@ Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass):
## Fase C — Cosa fa `Setup-WinBuild2025.ps1` (dentro la VM) ## Fase C — Cosa fa `Setup-WinBuild2025.ps1` (dentro la VM)
Eseguito automaticamente da Prepare in Fase B. Ordine ottimizzato per ridurre overhead: Eseguito automaticamente da Prepare in Fase B. Steps 1/2/3/4b/5b/5c sono
tutti i tweak UI/registro sono raggruppati **prima** di Windows Update (operazione lenta). **validation-only** — verificano che Deploy abbia già applicato l'hardening OS.
Ogni step ha validazione `Assert-Step`. Steps 4/5/6/6b/7-10 eseguono la CI customization effettiva.
Ogni step ha validazione `Assert-Step` (throw on failure).
``` ```
Step 1 Firewall off (Domain/Private/Public) ── primo, rimuove blocchi Step 1 Firewall: validation only — Deploy disabled all profiles [Assert-Step]
Step 2 Defender + Antimalware off (Set-MpPreference + GPO) Step 2 Defender: validation only — Deploy set DisableAntiSpyware GPO + RTP off [Assert-Step]
── prima di WinRM e WU Step 3 WinRM: validation only — Deploy set Basic/Unencrypted/MaxMem/Timeout [Assert-Step]
Step 3 WinRM hardening (Basic, AllowUnencrypted, 2 GB shell mem, 2h timeout) Step 4 Build user account (ci_build, admin, no expire) ── crea utente CI
Step 4 Build user account (ci_build, admin, no expire) ── prima di WU Step 4b UAC: validation only — Deploy set EnableLUA=0 + TokenFilterPolicy [Assert-Step]
Step 4b UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU
Step 5b Explorer LaunchTo=1 (This PC) Step 5b Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive [Assert-Step]
Step 5c CI UX hardening: Server Manager off, DisableCAD, OOBE/telemetry off Step 5c CI UX hardening: validation only — Deploy set lock screen, SM policy/task,
Step 6 Windows Update via Scheduled Task SYSTEM ── senza scan Defender, C:\CI presente DisableCAD, OOBE, telemetry, autologin Administrator [Assert-Step]
Step 6 Windows Update via Scheduled Task SYSTEM ── clears Deploy GPO locks, runs WU
Step 6b Windows Update permanently disabled (post-update lockdown) Step 6b Windows Update permanently disabled (post-update lockdown)
Step 7 .NET SDK install (channel via dotnet-install.ps1) Step 7 .NET SDK install (channel via dotnet-install.ps1)
Step 8 Python install (3.13.3, all-users, C:\Python) Step 8 Python install (3.13.3, all-users, C:\Python)
@@ -168,41 +176,36 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
### Razionale dell'ordine ### Razionale dell'ordine
- **Firewall first** — Windows blocca alcune chiamate WinRM finché non è classificato il - **Steps 1-3 (validazioni)** — verificano prima le precondizioni Deploy (firewall off,
profilo di rete. Off subito → niente race condition durante Step 3. Defender off, WinRM pronto) in modo che i failure emergano subito, non a metà toolchain.
- **Defender second** — disabilitato prima di WinRM e WU. WU scarica/installa update - **User + UAC (4/4b)** — crea `ci_build`; valida UAC off (set da Deploy); UAC off prima
senza scan (5-15 min risparmiati). Step 7-9 (download + install ~200-400 MB di di dir/tool evita prompt di elevazione negli step 7-9.
payload .NET/Python/VS) non vengono scansionati.
- **WinRM third** — già non interferito da firewall/defender. Hardening pulito.
- **User + UAC (4/4b)** — operazioni registro rapide; UAC off prima di dir/tool evita
prompt di elevazione; nessun motivo di aspettare WU.
- **CI dirs prima di WU (Step 5)** — il worker WU scrive file temp in `C:\CI\` - **CI dirs prima di WU (Step 5)** — il worker WU scrive file temp in `C:\CI\`
(`wu_worker.ps1`, `wu_result.txt`, ecc.); la directory deve esistere prima. (`wu_worker.ps1`, `wu_result.txt`, ecc.); la directory deve esistere prima.
- **UI tweaks (5b/5c)** — tutti i tweak registro raggruppati prima del WU (lento); - **UI tweaks (5b/5c)** — validazioni registro rapide; raggruppate prima di WU (lento).
scritture veloci, nessuna dipendenza da WU o toolchain. - **WU (Step 6)** — clears i GPO lock di Deploy (Step A), avvia COM API via SYSTEM task,
- **WU sixth** — gira con tutto già off + `C:\CI` presente; condizioni ottimali. poi Step 6b re-disabilita tutto → snapshot cattura WU permanentemente off.
- **WU disable subito dopo** — snapshot cattura WU permanentemente off.
- **Toolchain (7-9) last** — WU completato, nessun scan su payload installer. - **Toolchain (7-9) last** — WU completato, nessun scan su payload installer.
### Validazioni per step (sintesi) ### Validazioni per step (sintesi)
| Step | Check chiave | | Step | Natura | Check chiave |
| ---- | ------------------------------------------------------------------------------- | | ---- | ---------- | ------------------------------------------------------------------------------ |
| 1 | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` | | 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` |
| 2 | 6 GPO key + `Set-MpPreference DisableRealtimeMonitoring=true` + ExclusionPath | | 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) |
| 3 | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 | | 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 |
| 4 | User exists, enabled, PasswordNeverExpires, member of Administrators | | 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Ogni dir Test-Path -PathType Container | | 5 | Operativo | Ogni dir Test-Path -PathType Container |
| 5b | HKCU LaunchTo=1 | | 5b | Assert | HKCU LaunchTo=1 |
| 5c | ServerManager machine policy DoNotOpenAtLogon=1, HKCU DoNotOpenServerManagerAtLogon=1, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0 | | 5c | Assert+Op | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0; AutoAdminLogon=1+DefaultUserName=Administrator re-affermati se assenti (Windows OOBE può resettarli al primo boot) |
| 6 | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) | | 6 | Operativo | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
| 6b | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 | | 6b | Operativo | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
| 7 | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` | | 7 | Operativo | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` |
| 8 | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` | | 8 | Operativo | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` |
| 9 | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente | | 9 | Operativo | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente |
| 10 | dotnet+python+msbuild tutti risolvibili (throw, non più warn) | | 10 | Operativo | dotnet+python+msbuild tutti risolvibili (throw) |
| Final| WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` | | Final| Cross-cut | WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` |
--- ---
@@ -219,6 +222,35 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
--- ---
## Validation scripts standalone
Due script per validare il guest in punti specifici del flusso, indipendentemente da
Prepare. Utili per debug, re-verifica dopo modifiche manuali, e CI automatizzata.
### Dopo Deploy — prima di Prepare
```powershell
.\template\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
```
Controlla: Firewall off, Defender GPO, WinRM (Running/Automatic/AllowUnencrypted/Basic/MaxMem),
UAC off, Explorer LaunchTo, NoLockScreen, Server Manager (policy+HKLM+task+HKCU),
DisableCAD, OOBE, AllowTelemetry, **AutoAdminLogon=1**, WU GPO, wuauserv/UsoSvc=Manual.
### Dopo Prepare — prima di snapshot
```powershell
.\template\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
```
Tutti i check Deploy + ci_build (exists/enabled/admin/PasswordNeverExpires),
`C:\CI\{build,output,scripts}`, wuauserv/UsoSvc=Disabled, WU GPO,
.NET SDK channel match, Python version match, MSBuild present, no leftover installer files.
Exit 0 = tutto OK. Exit 1 = almeno un check fallito (output indica quale).
---
## Fase E — Verifica finale ## Fase E — Verifica finale
Dall'host: Dall'host:
+268 -89
View File
@@ -14,11 +14,11 @@
via IMAPI2FS COM (built-in Windows, zero deps) via IMAPI2FS COM (built-in Windows, zero deps)
Step 4 — Create the VMDK (80 GB single-file thin) via vmware-vdiskmanager Step 4 — Create the VMDK (80 GB single-file thin) via vmware-vdiskmanager
Step 5 — Generate the .vmx (UEFI no SecureBoot, NVMe disk, e1000e NIC, Step 5 — Generate the .vmx (UEFI no SecureBoot, NVMe disk, e1000e NIC,
three CD-ROMs: Win install + autounattend + VMware Tools) two CD-ROMs: Win install + autounattend; Tools bundled inside autounattend)
Step 6 — Power on the VM (vmrun start nogui) Step 6 — Power on the VM (vmrun start gui/nogui — async to avoid Workstation hang)
Step 7 — Wait for the install_complete.flag file inside the guest Step 7 — Wait for the install_complete.flag file inside the guest
(created by post-install.ps1 once Tools install finishes) (created by post-install.ps1 once Tools install finishes)
Step 8 — Graceful soft-stop, swap NIC e1000e → vmxnet3 in the .vmx Step 8 — Graceful soft-stop, remove CD-ROM controller + USB from VMX, swap NIC e1000e → vmxnet3
Step 9 — Power on, wait for guest IP via vmrun (vmxnet3 verified) Step 9 — Power on, wait for guest IP via vmrun (vmxnet3 verified)
Step 10 — Take snapshot named $SnapshotName Step 10 — Take snapshot named $SnapshotName
Final — Print summary (VM path, IP, snapshot name) Final — Print summary (VM path, IP, snapshot name)
@@ -36,13 +36,22 @@
Hardening done by post-install.ps1 inside the guest: Hardening done by post-install.ps1 inside the guest:
- UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1) - UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
- Defender real-time protection off - Defender real-time protection off + DisableAntiSpyware=1 GPO
- DiagTrack/Telemetry off - DiagTrack/Telemetry off
- Windows Update services disabled - Windows Update GPO locks (NoAutoUpdate=1, DisableWindowsUpdateAccess=1);
- RDP enabled (no NLA, lab only) + firewall rule services remain Manual — Setup-WinBuild2025 Step 6b permanently disables
- WinRM enabled HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth, them after applying updates (§7.2 Strategy B)
AllowUnencrypted, TrustedHosts=* - Windows Firewall all profiles disabled (VMnet8 NAT lab; Setup Step 1 validates)
- RDP enabled (no NLA, lab only) + firewall rules for RDP/ICMP/WinRM-HTTPS
- WinRM HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth,
AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25
- ICMPv4 inbound allowed - ICMPv4 inbound allowed
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
Setup-WinBuild2025 Step 5b/5c validate all of these
- Taskbar search bar hidden, Task View button hidden
- Windows Terminal set as default terminal (DelegationConsole/Terminal HKCU + Default hive)
- Azure Arc auto-start, scheduled tasks, and himds service disabled
- VMware Tools installed (Complete) from Tools ISO - VMware Tools installed (Complete) from Tools ISO
- install_complete.flag dropped in C:\Windows\Temp - install_complete.flag dropped in C:\Windows\Temp
@@ -50,7 +59,8 @@
Path to the Windows Server 2025 install ISO. Path to the Windows Server 2025 install ISO.
.PARAMETER ToolsISO .PARAMETER ToolsISO
Path to the VMware Tools ISO (windows.iso). Path to the VMware Tools ISO (windows.iso). Contents are extracted and bundled
inside the autounattend ISO at build time (Step 3); not mounted as a separate CD-ROM.
.PARAMETER VMXPath .PARAMETER VMXPath
Full path of the .vmx file to create. The folder is created if missing. Full path of the .vmx file to create. The folder is created if missing.
@@ -138,7 +148,7 @@ param(
# ── Hardware ── # ── Hardware ──
[int] $DiskSizeGB = 80, [int] $DiskSizeGB = 80,
[int] $MemoryMB = 6144, [int] $MemoryMB = 6144,
[int] $VCPUCount = 2, [int] $VCPUCount = 4,
[int] $CoresPerSocket = 2, [int] $CoresPerSocket = 2,
# ── Locale ── # ── Locale ──
@@ -159,7 +169,14 @@ param(
# When set, vmrun powers on the VM with the Workstation GUI visible. # When set, vmrun powers on the VM with the Workstation GUI visible.
# Default: headless (nogui) for unattended pipelines. # Default: headless (nogui) for unattended pipelines.
[switch] $ShowGui [switch] $ShowGui,
# Skip straight to this step number. All derived paths are still computed;
# artifacts from skipped steps must already exist on disk.
# Steps: 1=preflight 2=render XML 3=autounattend ISO 4=VMDK+noPromptISO
# 5=VMX 6=power on 7=wait flag 8=stop+NIC 9=power on vmxnet3 10=snapshot
[ValidateRange(1,10)]
[int] $StartFromStep = 1
) )
$vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' } $vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' }
@@ -172,8 +189,13 @@ $ErrorActionPreference = 'Stop'
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
function Write-Step { function Write-Step {
param([string]$Message) param([string] $Message, [int] $Step = 0)
Write-Host "`n=== $Message ===" -ForegroundColor Cyan $skipped = $Step -gt 0 -and $script:StartFromStep -gt $Step
if ($skipped) {
Write-Host "`n=== $Message === [SKIP]" -ForegroundColor DarkGray
} else {
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
}
} }
function Assert-Step { function Assert-Step {
@@ -344,8 +366,14 @@ function New-WinIsoNoPrompt {
function Invoke-Vmrun { function Invoke-Vmrun {
param([Parameter(Mandatory)] [string[]] $Arguments, param([Parameter(Mandatory)] [string[]] $Arguments,
[switch] $IgnoreErrors) [switch] $IgnoreErrors,
[switch] $Async)
$vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe' $vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe'
if ($Async) {
# vmrun start can block indefinitely on some Workstation versions; fire async.
Start-Process -FilePath $vmrun -ArgumentList $Arguments -NoNewWindow
return
}
$stdout = & $vmrun @Arguments 2>&1 $stdout = & $vmrun @Arguments 2>&1
$code = $LASTEXITCODE $code = $LASTEXITCODE
if ($code -ne 0 -and -not $IgnoreErrors) { if ($code -ne 0 -and -not $IgnoreErrors) {
@@ -371,15 +399,80 @@ function Set-VmxKey {
Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII
} }
function Remove-VmxLines {
param([Parameter(Mandatory)] [string] $VmxPath,
[Parameter(Mandatory)] [string] $KeyPrefix)
$pat = "^\s*$([regex]::Escape($KeyPrefix))"
$lines = Get-Content -LiteralPath $VmxPath | Where-Object { $_ -notmatch $pat }
Set-Content -LiteralPath $VmxPath -Value $lines -Encoding ASCII
}
function Test-VmInList {
param([Parameter(Mandatory)] [string] $VmxPath)
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
$forward = $VmxPath -replace '\\', '/'
$back = $VmxPath -replace '/', '\'
return ($list -match [regex]::Escape($forward)) -or ($list -match [regex]::Escape($back))
}
function Invoke-VmrunBounded {
# Run vmrun synchronously but kill the vmrun process after $TimeoutSeconds.
# Useful for 'stop soft': vmrun blocks until guest shuts down, but the ACPI
# signal is sent within seconds — killing vmrun doesn't cancel the guest shutdown.
param([Parameter(Mandatory)] [string[]] $Arguments,
[int] $TimeoutSeconds = 30)
$exe = Join-Path $VMwareWorkstationDir 'vmrun.exe'
$proc = Start-Process -FilePath $exe -ArgumentList $Arguments -NoNewWindow -PassThru
$null = $proc.WaitForExit($TimeoutSeconds * 1000)
if (-not $proc.HasExited) { $proc.Kill() }
}
function Stop-Vm {
param([Parameter(Mandatory)] [string] $VmxPath,
[int] $SoftTimeoutMinutes = 5)
# Send soft-stop (ACPI); vmrun may block until guest finishes — kill vmrun after 30 s.
# Guest OS continues shutdown independently once the signal is received.
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'soft') -TimeoutSeconds 30
$deadline = (Get-Date).AddMinutes($SoftTimeoutMinutes)
do {
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deadline -and (Test-VmInList -VmxPath $VmxPath))
if (Test-VmInList -VmxPath $VmxPath) {
Write-Host ' soft stop timed out — hard stop'
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'hard') -TimeoutSeconds 15
Start-Sleep -Seconds 5
}
}
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 1: Pre-flight validation # Step 1: Pre-flight validation
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 1: pre-flight validation' # ── Derived paths — always computed regardless of -StartFromStep ─────────────
$vmrunPath = Join-Path $VMwareWorkstationDir 'vmrun.exe' $vmrunPath = Join-Path $VMwareWorkstationDir 'vmrun.exe'
$vdiskMgr = Join-Path $VMwareWorkstationDir 'vmware-vdiskmanager.exe' $vdiskMgr = Join-Path $VMwareWorkstationDir 'vmware-vdiskmanager.exe'
$scriptRoot = Split-Path -Parent $PSCommandPath $scriptRoot = Split-Path -Parent $PSCommandPath
$xmlTemplate = Join-Path $scriptRoot 'autounattend.template.xml' $xmlTemplate = Join-Path $scriptRoot 'autounattend.template.xml'
$vmDir = Split-Path -Parent $VMXPath
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk'
$vmdkPath = Join-Path $vmDir $vmdkName
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
$stagingDir = Join-Path $vmDir '_autounattend_staging'
$guestOS = 'windows2019srvnext-64'
$guestIP = $null # set in Step 9; pre-init for strict-mode safety
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
$srcDir = Split-Path -Parent $WinISO
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
}
if ($StartFromStep -gt 1) {
Write-Host " [SKIP] Steps 1..$($StartFromStep - 1) — starting from Step $StartFromStep" -ForegroundColor Yellow
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: pre-flight validation
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 1: pre-flight validation' -Step 1
if ($StartFromStep -le 1) {
Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath } Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath }
Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr } Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr }
@@ -389,26 +482,21 @@ Assert-Step 'PreFlight' "autounattend template exists: $xmlTemplate" { Test-Pat
Assert-Step 'PreFlight' "ComputerName valid (1-15 chars, no spaces)" { Assert-Step 'PreFlight' "ComputerName valid (1-15 chars, no spaces)" {
$ComputerName -match '^[A-Za-z0-9-]{1,15}$' $ComputerName -match '^[A-Za-z0-9-]{1,15}$'
} }
$vmDir = Split-Path -Parent $VMXPath
if (-not (Test-Path $vmDir)) { if (-not (Test-Path $vmDir)) {
New-Item -ItemType Directory -Path $vmDir -Force | Out-Null New-Item -ItemType Directory -Path $vmDir -Force | Out-Null
} }
Assert-Step 'PreFlight' "VM directory ready: $vmDir" { Test-Path $vmDir } Assert-Step 'PreFlight' "VM directory ready: $vmDir" { Test-Path $vmDir }
if (Test-Path $VMXPath) { if (Test-Path $VMXPath) {
throw "[PreFlight] $VMXPath already exists. Remove the existing VM folder before re-running." throw "[PreFlight] $VMXPath already exists. Remove the existing VM folder before re-running."
} }
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk' } # end Step 1
$vmdkPath = Join-Path $vmDir $vmdkName
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
$stagingDir = Join-Path $vmDir '_autounattend_staging'
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 2: render autounattend.xml + post-install.ps1 # Step 2: render autounattend.xml + post-install.ps1
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 2: render autounattend.xml + post-install.ps1' Write-Step 'Step 2: render autounattend.xml + post-install.ps1' -Step 2
if ($StartFromStep -le 2) {
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force } if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
@@ -506,15 +594,23 @@ sc.exe config DiagTrack start= disabled | Out-Null
sc.exe stop DiagTrack | Out-Null sc.exe stop DiagTrack | Out-Null
L 'Telemetry disabled' L 'Telemetry disabled'
# Windows Update services disabled # Windows Update GPO locks services left Manual (§7.2 Strategy B)
foreach (`$svc in 'wuauserv','UsoSvc','WaaSMedicSvc') { # Set both WU services to Manual so they can be invoked on-demand by Setup
sc.exe config `$svc start= disabled | Out-Null # Step 6 (WU COM API via SYSTEM task) but won't auto-start between Deploy and Setup.
sc.exe stop `$svc | Out-Null # UsoSvc defaults to Automatic on Windows Server 2025; must be explicit here.
} # GPO keys below also block background triggers. Step 6b permanently disables both.
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null Set-Service -Name wuauserv -StartupType Manual -ErrorAction SilentlyContinue
L 'Windows Update disabled' Set-Service -Name UsoSvc -StartupType Manual -ErrorAction SilentlyContinue
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' /v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f | Out-Null
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
L 'Windows Update GPO locks set + wuauserv/UsoSvc Manual (Setup Step 6b permanently disables)'
# Firewall: profiles stay default; allow RDP + ICMP + WinRM HTTPS # Firewall: all profiles disabled + allow RDP + ICMP + WinRM HTTPS
# Disabling all profiles removes any block on subsequent WinRM config and WU
# downloads. This VM lives on VMnet8 NAT behind the VMware NAT gateway no
# inbound exposure from outside the host. Setup-WinBuild2025 Step 1 validates.
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False
L 'Windows Firewall disabled on all profiles'
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' /v fDenyTSConnections /t REG_DWORD /d 0 /f | Out-Null reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' /v fDenyTSConnections /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' /v UserAuthentication /t REG_DWORD /d 0 /f | Out-Null reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' /v UserAuthentication /t REG_DWORD /d 0 /f | Out-Null
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue
@@ -533,6 +629,11 @@ try {
New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null
L 'WinRM HTTPS listener configured' L 'WinRM HTTPS listener configured'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" } } catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null
Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>`$null
Set-Service -Name WinRM -StartupType Automatic 3>`$null
L 'WinRM shell limits: MaxMemory=2048MB IdleTimeout=2h MaxProcesses=25; StartType=Automatic'
# Server Manager / WAC popup / first-logon UX # Server Manager / WAC popup / first-logon UX
# Server Manager auto-start off machine-wide + current Administrator + Default # Server Manager auto-start off machine-wide + current Administrator + Default
@@ -549,6 +650,24 @@ reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v Disa
# OOBE privacy experience suppressed # OOBE privacy experience suppressed
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Server Manager + WAC + CAD + OOBE prompts disabled' L 'Server Manager + WAC + CAD + OOBE prompts disabled'
# Supplemental UX hardening validated by Setup-WinBuild2025 Step 5c:
# Lock screen: suppress at CI console sessions (no interactive user ever logs in)
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Personalization' /v NoLockScreen /t REG_DWORD /d 1 /f | Out-Null
# Server Manager GPO policy key (authoritative path on WS2025; HKLM key above is not always honoured)
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' /v DoNotOpenAtLogon /t REG_DWORD /d 1 /f | Out-Null
# Server Manager scheduled task the actual launcher on WS2025; registry alone insufficient
schtasks /Change /TN '\Microsoft\Windows\Server Manager\ServerManager' /Disable 2>&1 | Out-Null
# DisableCAD in Winlogon (belt-and-suspenders with Policies\System key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
# Autologin Administrator auto-signs in after every boot (CI template convenience).
# Password stored plaintext in registry; acceptable for isolated VMnet8 NAT lab VM.
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v AutoAdminLogon /t REG_SZ /d 1 /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultUserName /t REG_SZ /d Administrator /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultPassword /t REG_SZ /d "$AdminPassword" /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultDomainName /t REG_SZ /d '.' /f | Out-Null
# OOBE non-policy key (supplements the Policies\Windows\OOBE key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Lock screen, SM GPO policy + task, CAD Winlogon, autologin Administrator, OOBE non-policy key set'
# IE Enhanced Security Configuration off (Admin) # IE Enhanced Security Configuration off (Admin)
# Long-known CI papercut: ESC blocks every URL in admin sessions. # Long-known CI papercut: ESC blocks every URL in admin sessions.
@@ -557,6 +676,13 @@ reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37E
Stop-Process -Name iexplore -ErrorAction SilentlyContinue Stop-Process -Name iexplore -ErrorAction SilentlyContinue
L 'IE ESC disabled' L 'IE ESC disabled'
# Remove drive letter from EFI System Partition
# Windows setup sometimes assigns 'S:' to the ESP making it visible in Explorer.
Get-Partition | Where-Object { `$_.DriveLetter -eq 'S' } | ForEach-Object {
`$_ | Remove-PartitionAccessPath -AccessPath 'S:\' -ErrorAction SilentlyContinue
}
L 'EFI partition drive letter removed (if any)'
# Hibernate off (frees ~RAM-size on C:, no need on VMs) # Hibernate off (frees ~RAM-size on C:, no need on VMs)
powercfg /h off 2>&1 | Out-Null powercfg /h off 2>&1 | Out-Null
# Power plan High Performance never sleep, never throttle CPU # Power plan High Performance never sleep, never throttle CPU
@@ -591,11 +717,51 @@ foreach (`$task in @(
} }
L 'CEIP / feedback scheduled tasks disabled' L 'CEIP / feedback scheduled tasks disabled'
# Azure Arc auto-start / tray icon off
# Server 2025 ships with Azure Arc integration enabled; suppress for CI VMs.
foreach (`$arcTask in @(
'\Microsoft\Azure Arc\Onboarding\EnableAzureArcSetup',
'\Microsoft\Azure Arc\Onboarding\ArcScan',
'\Microsoft\AzureArcSeamlessOnboarding\ArcSetup'
)) {
schtasks /Change /TN `$arcTask /Disable 2>&1 | Out-Null
}
foreach (`$arcSvc in 'himds','ArcProxyAgent') {
`$s = Get-Service -Name `$arcSvc -ErrorAction SilentlyContinue
if (`$s) {
sc.exe config `$arcSvc start= disabled | Out-Null
sc.exe stop `$arcSvc 2>&1 | Out-Null
}
}
reg add 'HKLM\SOFTWARE\Microsoft\AzureArc' /v OnboardingState /t REG_DWORD /d 4 /f 2>&1 | Out-Null
reg delete 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' /v AzureArcSetup /f 2>&1 | Out-Null
L 'Azure Arc auto-start disabled'
# Explorer UX: show file extensions + open to This PC # Explorer UX: show file extensions + open to This PC
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v LaunchTo /t REG_DWORD /d 1 /f | Out-Null reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v LaunchTo /t REG_DWORD /d 1 /f | Out-Null
L 'Explorer UX tweaks applied (HKCU)' L 'Explorer UX tweaks applied (HKCU)'
# Taskbar / terminal UX tweaks
# SearchboxTaskbarMode=0 hide search bar
# ShowTaskViewButton=0 hide Task View button
# DelegationConsole/Terminal Windows Terminal as default terminal
# LayoutModification.xml pre-pin Windows Terminal for new user profiles
# HKCU (SYSTEM context at FirstLogon)
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg add 'HKCU\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
# Default hive Administrator + any future user inherits on first interactive logon
reg load HKU\DefaultUser2 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg unload HKU\DefaultUser2 2>&1 | Out-Null
L 'Taskbar: search hidden, task view hidden, WT default terminal'
# Wait for VMware Tools install to settle (kicked off at top) # Wait for VMware Tools install to settle (kicked off at top)
# Wrapper returned immediately while msiexec runs async. Poll until: # Wrapper returned immediately while msiexec runs async. Poll until:
# (a) no setup/setup64/vminst wrapper process (excludes system msiexec /V) # (a) no setup/setup64/vminst wrapper process (excludes system msiexec /V)
@@ -634,9 +800,11 @@ if (`$toolsExe) {
New-Item -ItemType File -Path 'C:\Windows\Temp\install_complete.flag' -Force | Out-Null New-Item -ItemType File -Path 'C:\Windows\Temp\install_complete.flag' -Force | Out-Null
L 'install_complete.flag dropped' L 'install_complete.flag dropped'
# Reboot to apply Tools/UAC/services state cleanly # Shut down (not restart) so host Step 8 can swap NIC and snapshot
L 'rebooting in 15s' # Delay 60s: host polls every 30s, needs one cycle after flag appears.
shutdown /r /t 15 /f /c "post-install complete" # Step 8 sends vmrun stop soft on top of this Windows handles both.
L 'shutting down in 60s'
shutdown /s /t 60 /f /c "post-install complete"
"@ "@
$psOut = Join-Path $stagingDir 'post-install.ps1' $psOut = Join-Path $stagingDir 'post-install.ps1'
Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8 Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8
@@ -660,10 +828,13 @@ Assert-Step 'Render' 'Tools setup bundled in staging\Tools' {
(Test-Path (Join-Path $toolsStage 'setup64.exe')) (Test-Path (Join-Path $toolsStage 'setup64.exe'))
} }
} # end Step 2
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 3: build autounattend ISO # Step 3: build autounattend ISO
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS' Write-Step 'Step 3: build autounattend ISO via IMAPI2FS' -Step 3
if ($StartFromStep -le 3) {
New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND' New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND'
Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso } Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso }
@@ -678,10 +849,13 @@ if (Test-Path $stagingDir) {
Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow
} }
} # end Step 3
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 4: create VMDK # Step 4: create VMDK + no-prompt Windows ISO
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)" Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)" -Step 4
if ($StartFromStep -le 4) {
# -t 0 = single growable file (thin); -a lsilogic affects descriptor only, # -t 0 = single growable file (thin); -a lsilogic affects descriptor only,
# the disk attaches to the NVMe controller defined in the VMX. # the disk attaches to the NVMe controller defined in the VMX.
@@ -693,13 +867,7 @@ Assert-Step 'Vmdk' "VMDK file present: $vmdkPath" { Test-Path $vmdkPath }
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 4b: rebuild Windows ISO with EFI no-prompt boot image # Step 4b: rebuild Windows ISO with EFI no-prompt boot image
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")' Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")' -Step 4
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
$srcDir = Split-Path -Parent $WinISO
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
}
$srcMtime = (Get-Item $WinISO).LastWriteTime $srcMtime = (Get-Item $WinISO).LastWriteTime
$rebuild = $true $rebuild = $true
@@ -717,21 +885,24 @@ if ($rebuild) {
Assert-Step 'NoPromptIso' "no-prompt ISO present: $WinISONoPromptPath" { Test-Path $WinISONoPromptPath } Assert-Step 'NoPromptIso' "no-prompt ISO present: $WinISONoPromptPath" { Test-Path $WinISONoPromptPath }
Assert-Step 'NoPromptIso' 'no-prompt ISO non-empty (>500 MB)' { (Get-Item $WinISONoPromptPath).Length -gt 500MB } Assert-Step 'NoPromptIso' 'no-prompt ISO non-empty (>500 MB)' { (Get-Item $WinISONoPromptPath).Length -gt 500MB }
} # end Step 4
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 5: generate VMX # Step 5: generate VMX
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 5: generate VMX' Write-Step 'Step 5: generate VMX' -Step 5
if ($StartFromStep -le 5) {
# guestOS: # guestOS:
# "windows2019srvnext-64" is the generic forward-compat ID for new Windows # "windows2019srvnext-64" is the generic forward-compat ID for new Windows
# Server releases on Workstation. Workstation 17.6+ accepts it for Server 2025. # Server releases on Workstation. Workstation 17.6+ accepts it for Server 2025.
# If your Workstation version supports the explicit ID, change it here. # If your Workstation version supports the explicit ID, change it here.
$guestOS = 'windows2019srvnext-64' # ($guestOS is set unconditionally in the derived-paths block above.)
# CD-ROM ordering (sata): # CD-ROM ordering (sata):
# sata0:0 = Windows install ISO (bootable) ← EFI picks this on first boot # sata0:0 = Windows install ISO (bootable) ← EFI picks this on first boot
# sata0:1 = autounattend ISO (non-bootable, scanned by Setup) # sata0:1 = autounattend ISO (non-bootable, scanned by Setup; Tools bundled inside)
# sata0:2 = VMware Tools ISO (Tools installer source) # Both disconnected in Step 8 after install — snapshot/clone boots clean with no ISOs.
$vmxLines = @( $vmxLines = @(
'.encoding = "windows-1252"', '.encoding = "windows-1252"',
'config.version = "8"', 'config.version = "8"',
@@ -774,7 +945,7 @@ $vmxLines = @(
"nvme0:0.fileName = `"$vmdkName`"", "nvme0:0.fileName = `"$vmdkName`"",
'nvme0:0.deviceType = "disk"', 'nvme0:0.deviceType = "disk"',
'', '',
'# Three SATA CD-ROMs: install / autounattend / Tools', '# Two SATA CD-ROMs: install ISO + autounattend ISO (Tools bundled in autounattend)',
'sata0.present = "TRUE"', 'sata0.present = "TRUE"',
'sata0.pciSlotNumber = "32"', 'sata0.pciSlotNumber = "32"',
'sata0:0.present = "TRUE"', 'sata0:0.present = "TRUE"',
@@ -785,10 +956,6 @@ $vmxLines = @(
'sata0:1.deviceType = "cdrom-image"', 'sata0:1.deviceType = "cdrom-image"',
"sata0:1.fileName = `"$autounattendIso`"", "sata0:1.fileName = `"$autounattendIso`"",
'sata0:1.startConnected = "TRUE"', 'sata0:1.startConnected = "TRUE"',
'sata0:2.present = "TRUE"',
'sata0:2.deviceType = "cdrom-image"',
"sata0:2.fileName = `"$ToolsISO`"",
'sata0:2.startConnected = "TRUE"',
'', '',
'# NIC: e1000e for install (broad compat); switched to vmxnet3 after Tools', '# NIC: e1000e for install (broad compat); switched to vmxnet3 after Tools',
'ethernet0.present = "TRUE"', 'ethernet0.present = "TRUE"',
@@ -822,19 +989,23 @@ $vmxLines = @(
Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII
Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath } Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath }
} # end Step 5
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 6: power on VM # Step 6: power on VM
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 6: power on VM (vmrun start nogui)' Write-Step "Step 6: power on VM (vmrun start $vmrunUiMode)" -Step 6
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null if ($StartFromStep -le 6) {
Start-Sleep -Seconds 5 Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors Start-Sleep -Seconds 10
Assert-Step 'PowerOn' 'VM appears in vmrun list' { $running -match [regex]::Escape($VMXPath) } Assert-Step 'PowerOn' 'VM appears in vmrun list' { Test-VmInList -VmxPath $VMXPath }
} # end Step 6
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 7: wait for install_complete.flag inside guest # Step 7: wait for install_complete.flag inside guest
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)" Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)" -Step 7
if ($StartFromStep -le 7) {
$deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes) $deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes)
$flagPath = 'C:\Windows\Temp\install_complete.flag' $flagPath = 'C:\Windows\Temp\install_complete.flag'
@@ -853,22 +1024,24 @@ while ((Get-Date) -lt $deadline) {
} }
Assert-Step 'GuestReady' "install_complete.flag observed in guest" { $flagSeen } Assert-Step 'GuestReady' "install_complete.flag observed in guest" { $flagSeen }
# ───────────────────────────────────────────────────────────────────────────── } # end Step 7
# Step 8: graceful soft-stop, swap NIC e1000e → vmxnet3
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 8: stop VM softly, swap NIC to vmxnet3'
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null # ─────────────────────────────────────────────────────────────────────────────
# Step 8: stop VM, disconnect ISOs, swap NIC e1000e → vmxnet3
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 8: stop VM, disconnect ISOs, swap NIC to vmxnet3' -Step 8
if ($StartFromStep -le 8) {
# Wait for VM to drop out of running list (max 3 min). Stop-Vm -VmxPath $VMXPath
$stopDeadline = (Get-Date).AddMinutes(3) Assert-Step 'Stop' 'VM no longer in vmrun list' { -not (Test-VmInList -VmxPath $VMXPath) }
do {
Start-Sleep -Seconds 5 # Remove CD-ROM controller and USB from VMX — not needed post-install.
$still = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors foreach ($prefix in 'sata0','usb','ehci','usb_xhci') {
} while ((Get-Date) -lt $stopDeadline -and ($still -match [regex]::Escape($VMXPath))) Remove-VmxLines -VmxPath $VMXPath -KeyPrefix $prefix
Assert-Step 'Stop' 'VM no longer in vmrun list' { }
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors Assert-Step 'CleanVmx' 'CD-ROM and USB entries removed from VMX' {
-not ($list -match [regex]::Escape($VMXPath)) $vmx = Get-Content -LiteralPath $VMXPath -Raw
($vmx -notmatch '(?m)^\s*sata0') -and ($vmx -notmatch '(?m)^\s*usb')
} }
Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3' Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3'
@@ -876,12 +1049,16 @@ Assert-Step 'NicSwap' 'ethernet0.virtualDev = vmxnet3' {
(Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$' (Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$'
} }
} # end Step 8
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 9: power on, verify vmxnet3 + DHCP IP # Step 9: power on, verify vmxnet3 + DHCP IP
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP' Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP' -Step 9
if ($StartFromStep -le 9) {
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
Start-Sleep -Seconds 10
$ipDeadline = (Get-Date).AddMinutes(10) $ipDeadline = (Get-Date).AddMinutes(10)
$guestIP = $null $guestIP = $null
do { do {
@@ -891,27 +1068,23 @@ do {
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+')) } while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
Assert-Step 'IPCheck' 'Guest IP obtained on vmxnet3' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' } Assert-Step 'IPCheck' 'Guest IP obtained on vmxnet3' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
} # end Step 9
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Step 10: shutdown → snapshot (cold) → power on # Step 10: shutdown → snapshot (cold) → power on
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on" Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on" -Step 10
if ($StartFromStep -le 10) {
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null Stop-Vm -VmxPath $VMXPath
$stopDeadline = (Get-Date).AddMinutes(3) Assert-Step 'Snapshot' 'VM powered off before snapshot' { -not (Test-VmInList -VmxPath $VMXPath) }
do {
Start-Sleep -Seconds 5
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
} while ((Get-Date) -lt $stopDeadline -and ($running -match [regex]::Escape($VMXPath)))
Assert-Step 'Snapshot' 'VM powered off before snapshot' {
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
-not ($list -match [regex]::Escape($VMXPath))
}
Invoke-Vmrun -Arguments @('-T','ws','snapshot',$VMXPath,$SnapshotName) | Out-Null Invoke-Vmrun -Arguments @('-T','ws','snapshot',$VMXPath,$SnapshotName) | Out-Null
$snaps = Invoke-Vmrun -Arguments @('-T','ws','listSnapshots',$VMXPath) $snaps = Invoke-Vmrun -Arguments @('-T','ws','listSnapshots',$VMXPath)
Assert-Step 'Snapshot' "Snapshot '$SnapshotName' present" { $snaps -match [regex]::Escape($SnapshotName) } Assert-Step 'Snapshot' "Snapshot '$SnapshotName' present" { $snaps -match [regex]::Escape($SnapshotName) }
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
Start-Sleep -Seconds 10
$ipDeadline = (Get-Date).AddMinutes(5) $ipDeadline = (Get-Date).AddMinutes(5)
do { do {
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
@@ -919,6 +1092,8 @@ do {
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+')) } while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
Assert-Step 'Snapshot' 'VM back online after snapshot' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' } Assert-Step 'Snapshot' 'VM back online after snapshot' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
} # end Step 10
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Final summary # Final summary
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -935,4 +1110,8 @@ Write-Host " RDP : mstsc /v:$guestIP"
Write-Host " WinRM HTTP : 5985" Write-Host " WinRM HTTP : 5985"
Write-Host " WinRM HTTPS : 5986 (self-signed)" Write-Host " WinRM HTTPS : 5986 (self-signed)"
Write-Host "" Write-Host ""
Write-Host " VM is powered on with vmxnet3 + DHCP. Live snapshot taken." -ForegroundColor Green Write-Host " Next steps (from template\ dir):"
Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP"
Write-Host " .\Prepare-WinBuild2025.ps1 -VMIPAddress $guestIP [-SkipWindowsUpdate]"
Write-Host ""
Write-Host " VM is powered on with vmxnet3 + DHCP. Snapshot '$SnapshotName' taken." -ForegroundColor Green
+20 -8
View File
@@ -380,6 +380,7 @@ try {
BuildPassword = $BuildPassword BuildPassword = $BuildPassword
BuildUsername = $BuildUsername BuildUsername = $BuildUsername
DotNetSdkVersion = $DotNetSdkVersion DotNetSdkVersion = $DotNetSdkVersion
AdminPassword = $AdminPassword
} }
if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true }
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true } if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
@@ -437,7 +438,17 @@ try {
Write-Host "[Prepare] Output from guest:" Write-Host "[Prepare] Output from guest:"
Write-Host "------------------------------------------------------------" Write-Host "------------------------------------------------------------"
$result = Invoke-GuestSetup -Session $session -ScriptPath $guestScriptPath -ScriptArgs $setupArgs $result = $null
try {
$result = Invoke-GuestSetup -Session $session -ScriptPath $guestScriptPath -ScriptArgs $setupArgs
} catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
# VM rebooted mid-update (WU triggered OS restart before Setup could return 3010).
# Treat as reboot-required: close dead session, fall through to the reboot-wait block.
Write-Host "[Prepare] WinRM transport error — VM rebooted mid-update. Treating as reboot-required..."
Remove-PSSession $session -ErrorAction SilentlyContinue
$session = $null
$result = 3010
}
Write-Host "------------------------------------------------------------" Write-Host "------------------------------------------------------------"
Write-Host "[Prepare] Iteration $iter exit code: $result" Write-Host "[Prepare] Iteration $iter exit code: $result"
@@ -448,18 +459,19 @@ try {
throw "Setup-WinBuild2025.ps1 exited with code $result" throw "Setup-WinBuild2025.ps1 exited with code $result"
} }
# exit 3010 → WU applied updates needing reboot. Reboot guest, wait, # exit 3010 (or transport error treated as 3010) → WU needs reboot.
# reopen session, loop. -SkipWindowsUpdate must NOT be set on retry — # Reboot guest if session still alive, wait for WinRM, reopen session, loop.
# we want the next iteration to drain remaining updates.
if ($iter -eq $maxWuIterations) { if ($iter -eq $maxWuIterations) {
throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)" throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)"
} }
Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..." Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..."
try { if ($session) {
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue try {
} catch { } Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue
Remove-PSSession $session -ErrorAction SilentlyContinue } catch { }
Remove-PSSession $session -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 30 # let WinRM tear down before polling Start-Sleep -Seconds 30 # let WinRM tear down before polling
Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..." Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..."
+97 -172
View File
@@ -8,40 +8,40 @@
This script is run ONE TIME inside the template VM before taking the This script is run ONE TIME inside the template VM before taking the
"BaseClean" snapshot. After the snapshot is taken, never modify the VM. "BaseClean" snapshot. After the snapshot is taken, never modify the VM.
Installs and configures (each step followed by Assert-Step validation): Prerequisite: Deploy-WinBuild2025.ps1 must have completed first (unattended OS
Step 1 — Firewall: all profiles disabled first — removes any block on subsequent steps install + post-install.ps1 hardening). Steps 1, 3, 4b, 5b, 5c are validation-only
— they assert that Deploy already set the expected state rather than re-applying it.
Steps 4, 5, 6/6b, 7-10 perform actual CI customisation.
Steps and Assert-Step validation:
Step 1 — Firewall: validation only — Deploy disabled all profiles.
Validation: Domain/Private/Public all Enabled=False Validation: Domain/Private/Public all Enabled=False
Step 2 — Defender state validation only (disabled by Deploy-WinBuild2025 Step 2 — Defender: validation only — Deploy set DisableAntiSpyware=1 GPO + RTP off.
during unattended install via DisableAntiSpyware GPO + RTP off). Validation: GPO DisableAntiSpyware=1
Validation: GPO DisableAntiSpyware=1. No exclusion paths — with Step 3 — WinRM: validation only — Deploy ran Enable-PSRemoting, Basic auth,
Defender off the scanner isn't running, so exclusions are moot. AllowUnencrypted, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Step 3 — WinRM: Basic auth, AllowUnencrypted, MaxMemoryPerShellMB=2048, 2h timeout
Firewall + Defender already off — no interference during config
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory
Step 4 — Local build user account ($BuildUsername, PasswordNeverExpires, Administrators) Step 4 — Local build user account ($BuildUsername, PasswordNeverExpires, Administrators)
Validation: user exists, enabled, PasswordNeverExpires, admin membership Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b — UAC disabled (EnableLUA=0, LocalAccountTokenFilterPolicy=1) Step 4b — UAC: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
Validation: both registry values verified Validation: both registry values verified
Step 5 — CI working directories: C:\CI\build, C:\CI\output, C:\CI\scripts Step 5 — CI working directories: C:\CI\build, C:\CI\output, C:\CI\scripts
Must run before Windows Update — WU worker writes temp files to C:\CI Must run before Windows Update — WU worker writes temp files to C:\CI
Validation: each path exists as Container Validation: each path exists as Container
Step 5b — Explorer LaunchTo=1 (This PC) for current user + Default profile hive Step 5b — Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive.
Validation: HKCU LaunchTo=1 Validation: HKCU LaunchTo=1
Step 5c — CI UX hardening (all UI tweaks before the long-running WU step): Step 5c — CI UX hardening: validation only — Deploy set lock screen, Server Manager
Server Manager auto-start off (machine policy + HKCU Administrator + (policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System
Default User hive → ci_build inherits at first logon) + Winlogon), OOBE/telemetry suppressed.
Ctrl+Alt+Del at login disabled (DisableCAD=1) — CI VMs never need it Validation: NoLockScreen, SM policy, SM task, DisableCAD, OOBE, telemetry
OOBE privacy/telemetry prompt suppressed (DisablePrivacyExperience=1, Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit).
AllowTelemetry=0) — prevents first-logon interactive prompt in clones Clears Deploy's GPO locks (Step A), starts services (Step D), runs COM API,
Validation: machine policy key, HKCU key, DisableCAD, OOBE policy, telemetry reports result code. Skipped with -SkipWindowsUpdate.
Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit)
Runs with Firewall + Defender off, C:\CI present — faster, no scan overhead
Validation: result code 0/2/3, no reboot required (else exit 3010) Validation: result code 0/2/3, no reboot required (else exit 3010)
Step 6b — Windows Update permanently disabled (post-update lockdown) Step 6b — Windows Update permanently disabled (post-update lockdown — always runs).
wuauserv + UsoSvc set to Disabled; GPO keys NoAutoUpdate=1, wuauserv + UsoSvc set to Disabled; GPO keys NoAutoUpdate=1,
DisableWindowsUpdateAccess=1; WU scheduled tasks disabled DisableWindowsUpdateAccess=1; WU scheduled tasks disabled.
Runs unconditionally (even with -SkipWindowsUpdate) so snapshot Snapshot captures WU permanently off — linked clones never auto-update.
captures WU off — linked clones never trigger background updates
Validation: wuauserv StartType=Disabled, GPO keys present Validation: wuauserv StartType=Disabled, GPO keys present
Step 7 — .NET SDK (channel $DotNetSdkVersion) via dotnet-install.ps1 Step 7 — .NET SDK (channel $DotNetSdkVersion) via dotnet-install.ps1
Validation: dotnet resolvable, version channel match, machine PATH Validation: dotnet resolvable, version channel match, machine PATH
@@ -50,28 +50,27 @@
Step 9 — Visual Studio Build Tools 2026 via Scheduled Task as SYSTEM Step 9 — Visual Studio Build Tools 2026 via Scheduled Task as SYSTEM
Validation: MSBuild.exe exists, executes, PATH, VC dir present Validation: MSBuild.exe exists, executes, PATH, VC dir present
Step 10 — Toolchain cross-check (dotnet, python, msbuild) Step 10 — Toolchain cross-check (dotnet, python, msbuild)
Throws on any missing tool (was: warn-only) Throws on any missing tool
Cleanup — Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM Cleanup — Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM
wuauserv already Disabled (Step 6b) — not restarted after cache clear wuauserv already Disabled (Step 6b) — not restarted after cache clear
SoftwareDistribution\Download cleared best-effort (WaaSMedicSvc may repopulate) Validation: wuauserv StartType Disabled
Validation: wuauserv StartType Disabled (SoftwareDistribution check removed —
WaaSMedicSvc tamper-protection restores files immediately)
Final — Pre-snapshot gate: 7 cross-cutting checks across all steps Final — Pre-snapshot gate: 7 cross-cutting checks across all steps
Throws if any check fails — prevents snapshot of broken state Throws if any check fails — prevents snapshot of broken state
Step ordering rationale: Step ordering rationale:
Firewall first — removes any network block before WinRM config and WU downloads Firewall (Step 1) — validates Deploy disabled all profiles; assertion before WinRM/WU
Defender second — eliminates scan overhead before WinRM, WU, and installer downloads Defender (Step 2) — validates Deploy disabled RTP; assertion before tool install
WinRM third configured clean with no Firewall/Defender interference WinRM (Step 3) validates Deploy configured WinRM; assertion before remote steps
User + UAC — quick registry ops; UAC off before dirs/tools avoids elevation prompts User (Step 4) — creates ci_build; must exist before UAC and dirs assertions
CI dirs — C:\CI must exist before WU worker writes temp files there UAC (Step 4b) — validates Deploy disabled UAC (elevation-free installs in 7-9)
UI tweaks — all fast registry writes batched before the slow WU step CI dirs (Step 5) — C:\CI must exist before WU worker writes temp files there
WU sixth — all prereqs (dirs, user, tweaks) done; Firewall+Defender off UI tweaks (5b/5c)validates Deploy UX settings; fast assertions before slow WU
WU disable — immediately after WU completes; snapshot captures WU permanently off WU (Step 6) — all prereqs validated; WU runs with Firewall+Defender already off
Toolchain — .NET / Python / VS last; WU done, no scan on installer payloads WU disable (Step 6b) — immediately after WU; snapshot captures WU permanently off
Toolchain (7-9) — .NET/Python/VS last; WU done, no scan on installer payloads
NOTE: Git is NOT installed. The host clones the repository and copies NOTE: Git is NOT installed by default. See §6.6 (Tier-1 Toolchain) in TODO.md
the source tree into the VM via WinRM (Option B isolation model). for the planned addition. The host clones and copies source via WinRM until then.
╔══════════════════════════════════════════════════════════════════════╗ ╔══════════════════════════════════════════════════════════════════════╗
║ NETWORK REQUIREMENT DURING SETUP ║ ║ NETWORK REQUIREMENT DURING SETUP ║
@@ -132,7 +131,11 @@ param(
[switch] $SkipWindowsUpdate, [switch] $SkipWindowsUpdate,
# Skip disk cleanup step (cleanmgr + DISM + cache clear) — speeds up re-runs during dev/debug # Skip disk cleanup step (cleanmgr + DISM + cache clear) — speeds up re-runs during dev/debug
[switch] $SkipCleanup [switch] $SkipCleanup,
# Administrator password — used only to write DefaultPassword for autologin.
# Optional: if empty, DefaultPassword registry value is left as-is (Deploy may have set it).
[string] $AdminPassword = ''
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
@@ -173,16 +176,12 @@ foreach ($f in $knownTempFiles) {
} }
} }
# ── Step 1: Firewall ────────────────────────────────────────────────────────── # ── Step 1: Firewall (validation only — disabled by Deploy-WinBuild2025) ──────
Write-Step "Disabling Windows Firewall (all profiles) — isolated lab VM" Write-Step "Firewall state (validation onlydisabled at deploy time)"
# Disable first: removes any block on subsequent WinRM config and WU downloads. # Deploy-WinBuild2025.ps1 post-install.ps1 called Set-NetFirewallProfile -Enabled False
# This VM lives on VMnet8 NAT behind the VMware NAT gateway — no inbound exposure # on all profiles. Validated here before WinRM and WU assertions proceed.
# from outside the host, so full disable is acceptable.
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False
Write-Host "Windows Firewall disabled on all profiles."
# Validation
foreach ($p in 'Domain','Private','Public') { foreach ($p in 'Domain','Private','Public') {
Assert-Step 'Firewall' "$p profile disabled" { Assert-Step 'Firewall' "$p profile disabled" {
(Get-NetFirewallProfile -Profile $p -ErrorAction Stop).Enabled -eq $false (Get-NetFirewallProfile -Profile $p -ErrorAction Stop).Enabled -eq $false
@@ -201,29 +200,13 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
((Get-ItemProperty -Path $key -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware -eq 1) ((Get-ItemProperty -Path $key -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware -eq 1)
} }
# ── Step 3: WinRM ───────────────────────────────────────────────────────────── # ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "Configuring WinRM" Write-Step "WinRM state (validation only — configured at deploy time)"
# Firewall and Defender already off — no interference during WinRM hardening. # Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth,
Enable-PSRemoting -Force -SkipNetworkProfileCheck 3>$null | Out-Null # AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25,
# and StartupType=Automatic. Validated here before the build user and toolchain steps.
# Allow unencrypted (HTTP/5985) — acceptable for an isolated lab network.
# Migrate to HTTPS/5986 with a self-signed cert for hardened environments.
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
# Increase shell memory and timeout limits for long builds
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>$null
Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>$null # 2 hours
# Ensure WinRM starts automatically
Set-Service -Name WinRM -StartupType Automatic 3>$null
Start-Service WinRM 3>$null
Write-Host "WinRM configured."
# Validation
Assert-Step 'WinRM' 'service Running' { Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running' (Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
} }
@@ -294,19 +277,13 @@ Assert-Step 'User' "user '$BuildUsername' member of Administrators" {
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername) [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
} }
# ── Step 4b: Disable UAC ───────────────────────────────────────────────────── # ── Step 4b: UAC (validation only — disabled by Deploy-WinBuild2025) ─────────
Write-Step "Disabling UAC (isolated lab VM)" Write-Step "UAC state (validation only — disabled at deploy time)"
# EnableLUA=0 disables UAC entirely — admin processes get full token without prompt. # Deploy-WinBuild2025.ps1 post-install.ps1 set EnableLUA=0 (no prompt for admin
# LocalAccountTokenFilterPolicy=1 ensures remote admin connections (WinRM) also # processes) and LocalAccountTokenFilterPolicy=1 (WinRM remote connections get full
# get an elevated token (avoids the filtered-token issue with runProgramInGuest). # elevated token, required for runProgramInGuest and remote admin commands).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name EnableLUA -Value 0 -Type DWord -Force
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name LocalAccountTokenFilterPolicy -Value 1 -Type DWord -Force
Write-Host "UAC disabled."
# Validation
$uacPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $uacPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Assert-Step 'UAC' 'EnableLUA=0' { Assert-Step 'UAC' 'EnableLUA=0' {
(Get-ItemProperty -Path $uacPolicyKey -Name EnableLUA -ErrorAction Stop).EnableLUA -eq 0 (Get-ItemProperty -Path $uacPolicyKey -Name EnableLUA -ErrorAction Stop).EnableLUA -eq 0
@@ -335,109 +312,32 @@ foreach ($dir in $ciDirs) {
} }
} }
# ── Step 5b: Explorer settings ─────────────────────────────────────────────── # ── Step 5b: Explorer settings (validation only — configured by Deploy-WinBuild2025) ──
Write-Step "Configuring Explorer defaults" Write-Step "Explorer defaults (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 set LaunchTo=1 for HKCU (Administrator)
# and the Default User hive (so ci_build and any new user inherits at first logon).
# Open Explorer to 'This PC' (Devices and Drives) instead of Quick Access.
# LaunchTo=1 = This PC, LaunchTo=2 = Quick Access (default)
$explorerAdvKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' $explorerAdvKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
Set-ItemProperty -Path $explorerAdvKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
# Also apply to the Default User profile so ci_build and any new user inherits it.
$defaultHive = 'C:\Users\Default\NTUSER.DAT'
$mountName = 'TempDefaultUser'
if (Test-Path $defaultHive) {
reg load "HKU\$mountName" $defaultHive | Out-Null
$defKey = "Registry::HKU\$mountName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
if (-not (Test-Path $defKey)) { New-Item -Path $defKey -Force | Out-Null }
Set-ItemProperty -Path $defKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$mountName" | Out-Null
Write-Host "Explorer: 'This PC' set for current user and default profile."
}
else {
Write-Host "Explorer: 'This PC' set for current user (default hive not found)."
}
# Validation
Assert-Step 'Explorer' "LaunchTo=1 (HKCU current user)" { Assert-Step 'Explorer' "LaunchTo=1 (HKCU current user)" {
(Get-ItemProperty -Path $explorerAdvKey -Name LaunchTo -ErrorAction Stop).LaunchTo -eq 1 (Get-ItemProperty -Path $explorerAdvKey -Name LaunchTo -ErrorAction Stop).LaunchTo -eq 1
} }
# ── Step 5c: CI UX hardening ──────────────────────────────────────────────── # ── Step 5c: CI UX hardening (validation only — configured by Deploy-WinBuild2025)
Write-Step "CI UX hardening (lock screen off, Server Manager off, no CAD, no OOBE)" Write-Step "CI UX hardening state (validation only — configured at deploy time)"
# All UI tweaks batched here — fast registry writes before the slow WU step. # Deploy-WinBuild2025.ps1 post-install.ps1 set: lock screen (NoLockScreen=1), Server
# Manager (policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System +
# Winlogon), OOBE DisablePrivacyExperience (policy + non-policy), AllowTelemetry=0.
# 1 — Lock screen: disable entirely (CI VMs never need a lock screen)
# NoLockScreen=1 suppresses the lock screen shown before login on console sessions.
$personPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' $personPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'
if (-not (Test-Path $personPolicyKey)) { New-Item -Path $personPolicyKey -Force | Out-Null } $smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
Set-ItemProperty -Path $personPolicyKey -Name 'NoLockScreen' -Value 1 -Type DWord -Force $smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
Write-Host "Lock screen disabled." $smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
# 2 — Server Manager: do not auto-open at logon
# Three layers needed on WS2025:
# a) Machine policy key (GPO path — read by ServerManager policy check)
# b) HKLM non-policy key (read directly by ServerManager.exe on WS2025)
# c) Scheduled Task '\Microsoft\Windows\Server Manager\ServerManager'
# — this is the actual launcher; registry alone is insufficient on WS2025
# d) HKCU + Default hive for per-user belt-and-suspenders
$smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
if (-not (Test-Path $smPolicyKey)) { New-Item -Path $smPolicyKey -Force | Out-Null }
Set-ItemProperty -Path $smPolicyKey -Name 'DoNotOpenAtLogon' -Value 1 -Type DWord -Force
$smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smHKLMKey)) { New-Item -Path $smHKLMKey -Force | Out-Null }
Set-ItemProperty -Path $smHKLMKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
Disable-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -ErrorAction SilentlyContinue | Out-Null
# Current session user (Administrator)
$smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smUserKey)) { New-Item -Path $smUserKey -Force | Out-Null }
Set-ItemProperty -Path $smUserKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
# Default User hive — ci_build and any future account inherits at first logon
$smDefaultHive = 'C:\Users\Default\NTUSER.DAT'
$smMountName = 'TempDefaultUserSM'
if (Test-Path $smDefaultHive) {
reg load "HKU\$smMountName" $smDefaultHive | Out-Null
$smDefKey = "Registry::HKU\$smMountName\SOFTWARE\Microsoft\ServerManager"
if (-not (Test-Path $smDefKey)) { New-Item -Path $smDefKey -Force | Out-Null }
Set-ItemProperty -Path $smDefKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$smMountName" | Out-Null
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU + Default hive set."
}
else {
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU set (Default hive not found)."
}
# 3 — Disable Ctrl+Alt+Del at interactive login
# Set in both locations: Winlogon (classic) + Policies\System (policy, authoritative on WS2025).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' `
-Name 'DisableCAD' -Value 1 -Type DWord -Force
$systemPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $systemPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Set-ItemProperty -Path $systemPolicyKey -Name 'DisableCAD' -Value 1 -Type DWord -Force $oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
Write-Host "Ctrl+Alt+Del at login disabled (Winlogon + Policies\System)." $dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
# 4 — Suppress OOBE privacy/telemetry prompt
$oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
if (-not (Test-Path $oobePolicyKey)) { New-Item -Path $oobePolicyKey -Force | Out-Null }
Set-ItemProperty -Path $oobePolicyKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$oobeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE'
if (-not (Test-Path $oobeKey)) { New-Item -Path $oobeKey -Force | Out-Null }
Set-ItemProperty -Path $oobeKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
if (-not (Test-Path $dcKey)) { New-Item -Path $dcKey -Force | Out-Null }
Set-ItemProperty -Path $dcKey -Name 'AllowTelemetry' -Value 0 -Type DWord -Force
Write-Host "OOBE privacy prompt and telemetry disabled."
# Validation
Assert-Step 'CIUX' 'lock screen disabled (NoLockScreen=1)' { Assert-Step 'CIUX' 'lock screen disabled (NoLockScreen=1)' {
(Get-ItemProperty -Path $personPolicyKey -Name NoLockScreen -ErrorAction Stop).NoLockScreen -eq 1 (Get-ItemProperty -Path $personPolicyKey -Name NoLockScreen -ErrorAction Stop).NoLockScreen -eq 1
} }
@@ -464,6 +364,25 @@ Assert-Step 'CIUX' 'OOBE policy DisablePrivacyExperience=1' {
Assert-Step 'CIUX' 'DataCollection AllowTelemetry=0' { Assert-Step 'CIUX' 'DataCollection AllowTelemetry=0' {
(Get-ItemProperty -Path $dcKey -Name AllowTelemetry -ErrorAction Stop).AllowTelemetry -eq 0 (Get-ItemProperty -Path $dcKey -Name AllowTelemetry -ErrorAction Stop).AllowTelemetry -eq 0
} }
# AutoAdminLogon: operativo — Deploy sets this, but Windows Server 2025 OOBE/first-boot
# tasks can reset AutoAdminLogon to 0 after the initial login. Re-affirm without touching
# DefaultPassword (already written by Deploy's post-install.ps1 and still present).
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
$wlNow = Get-ItemProperty -Path $wlKey -ErrorAction SilentlyContinue
if ($wlNow.AutoAdminLogon -ne '1' -or $wlNow.DefaultUserName -ne 'Administrator') {
Set-ItemProperty -Path $wlKey -Name AutoAdminLogon -Value '1' -Type String -Force
Set-ItemProperty -Path $wlKey -Name DefaultUserName -Value 'Administrator' -Type String -Force
Set-ItemProperty -Path $wlKey -Name DefaultDomainName -Value '.' -Type String -Force
}
# DefaultPassword written unconditionally when available — previous run may have set
# AutoAdminLogon=1 without knowing the password (AdminPassword was added later).
if ($AdminPassword -ne '') {
Set-ItemProperty -Path $wlKey -Name DefaultPassword -Value $AdminPassword -Type String -Force
}
Assert-Step 'CIUX' 'autologin Administrator enabled (AutoAdminLogon=1)' {
$wl = Get-ItemProperty -Path $wlKey -ErrorAction Stop
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
}
# ── Step 6: Windows Update ──────────────────────────────────────────────────── # ── Step 6: Windows Update ────────────────────────────────────────────────────
# Firewall + Defender already off: WU downloads and installs run without scanning. # Firewall + Defender already off: WU downloads and installs run without scanning.
@@ -1023,6 +942,12 @@ Write-Host "DISM exited (code $($dism.ExitCode))."
Write-Host "Disk cleanup complete." Write-Host "Disk cleanup complete."
# DISM StartComponentCleanup / WaaSMedicSvc may reset service StartType during cleanup.
# Re-enforce Disabled on both WU services after DISM completes.
foreach ($svc in 'wuauserv', 'UsoSvc') {
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
}
# Validation — leftover artifacts from this run should be gone # Validation — leftover artifacts from this run should be gone
# Note: do NOT assert SoftwareDistribution\Download empty. # Note: do NOT assert SoftwareDistribution\Download empty.
# WaaSMedicSvc (Windows Update Medic Service) is tamper-protected — cannot be stopped — # WaaSMedicSvc (Windows Update Medic Service) is tamper-protected — cannot be stopped —
@@ -1033,7 +958,7 @@ $sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -Fi
$sdMB = [math]::Round(($sdFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 1) $sdMB = [math]::Round(($sdFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 1)
Write-Host " SoftwareDistribution\Download: $($sdFiles.Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)." Write-Host " SoftwareDistribution\Download: $($sdFiles.Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
Assert-Step 'Cleanup' 'wuauserv StartType still Disabled after cache clear' { Assert-Step 'Cleanup' 'wuauserv StartType Disabled after cache clear' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled' (Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
} }
} # end if (-not $SkipCleanup) } # end if (-not $SkipCleanup)
+187
View File
@@ -0,0 +1,187 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Validates that Deploy-WinBuild2025.ps1 has correctly configured a guest VM.
.DESCRIPTION
Connects to the VM via WinRM (HTTP/Basic) and runs all checks that
Setup-WinBuild2025.ps1 Steps 1/2/3/4b/5b/5c assert as validation-only.
Use this after Deploy completes and before running Prepare/Setup.
.PARAMETER VMIPAddress
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
.PARAMETER AdminUsername
Admin account inside the VM (default: Administrator).
.PARAMETER AdminPassword
Password for the admin account. Prompted if omitted.
.EXAMPLE
.\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $VMIPAddress,
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $AdminPassword) {
$secure = Read-Host "Password for $AdminUsername@$VMIPAddress" -AsSecureString
$AdminPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
}
$cred = New-Object System.Management.Automation.PSCredential(
$AdminUsername,
(ConvertTo-SecureString $AdminPassword -AsPlainText -Force)
)
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force
}
Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
function Chk {
param([string]$Name, [scriptblock]$Test)
try { $ok = [bool](& $Test); $err = '' }
catch { $ok = $false; $err = $_.Exception.Message }
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
}
# ── Firewall ─────────────────────────────────────────────────────────
foreach ($p in Get-NetFirewallProfile) {
$n = $p.Name; $e = $p.Enabled
Chk "Firewall $n disabled" { $e -eq $false }
}
# ── Defender ─────────────────────────────────────────────────────────
Chk 'Defender GPO DisableAntiSpyware=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' `
-Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1
}
# ── WinRM ─────────────────────────────────────────────────────────────
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
}
Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
}
Chk 'WinRM MaxMemoryPerShellMB >= 2048' {
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048
}
# ── UAC ───────────────────────────────────────────────────────────────
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Chk 'UAC EnableLUA=0' {
(Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0
}
Chk 'UAC LocalAccountTokenFilterPolicy=1' {
(Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1
}
# ── Explorer ──────────────────────────────────────────────────────────
Chk 'Explorer LaunchTo=1 (HKCU)' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
-Name LaunchTo -EA Stop).LaunchTo -eq 1
}
# ── UX hardening ─────────────────────────────────────────────────────
Chk 'NoLockScreen=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' `
-Name NoLockScreen -EA Stop).NoLockScreen -eq 1
}
Chk 'Server Manager policy DoNotOpenAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' `
-Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1
}
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'Server Manager task Disabled' {
$t = Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -EA SilentlyContinue
(-not $t) -or ($t.State -eq 'Disabled')
}
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'DisableCAD=1 (Policies\System)' {
(Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1
}
Chk 'OOBE DisablePrivacyExperience=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' `
-Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1
}
Chk 'DataCollection AllowTelemetry=0' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' `
-Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0
}
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' {
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
}
# ── Windows Update GPO + services ─────────────────────────────────────
Chk 'WU GPO NoAutoUpdate=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' `
-Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1
}
Chk 'WU GPO DisableWindowsUpdateAccess=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' `
-Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1
}
Chk 'wuauserv StartType=Manual' { (Get-Service wuauserv -EA Stop).StartType -eq 'Manual' }
Chk 'UsoSvc StartType=Manual' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Manual' }
return $results.ToArray()
}
Write-Host ''
$failed = 0
foreach ($r in $checks) {
if ($r.Pass) {
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
} else {
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
$failed++
}
}
Write-Host ''
if ($failed -eq 0) {
Write-Host "All $($checks.Count) Deploy checks passed." -ForegroundColor Green
exit 0
} else {
Write-Host "$failed / $($checks.Count) Deploy checks FAILED." -ForegroundColor Red
exit 1
}
} finally {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
}
+374
View File
@@ -0,0 +1,374 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Validates full post-Setup state of a template VM (Deploy + Setup).
.DESCRIPTION
Connects to the VM via WinRM (HTTP/Basic) and runs all checks for the
final BaseClean snapshot state: Deploy OS hardening + Setup CI toolchain.
Run this after Prepare-WinBuild2025.ps1 completes (exit 0) to confirm
the VM is ready for snapshot and production use.
.PARAMETER VMIPAddress
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
.PARAMETER AdminUsername
Admin account inside the VM (default: Administrator).
.PARAMETER AdminPassword
Password for the admin account. Prompted if omitted.
.PARAMETER BuildUsername
CI build account created by Setup (default: ci_build).
.PARAMETER DotNetChannel
Expected .NET SDK channel prefix (default: 10.0).
.PARAMETER PythonVersion
Expected exact Python version string (default: 3.13.3).
.PARAMETER Remediate
If set, automatically applies fixes for known remediable failures
(AutoAdminLogon, wuauserv/UsoSvc StartType=Disabled) then re-runs all checks.
.EXAMPLE
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
.EXAMPLE
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129 -Remediate
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $VMIPAddress,
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword,
[string] $BuildUsername = 'ci_build',
[string] $DotNetChannel = '10.0',
[string] $PythonVersion = '3.13.3',
[switch] $Remediate
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $AdminPassword) {
$secure = Read-Host "Password for $AdminUsername@$VMIPAddress" -AsSecureString
$AdminPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
}
$cred = New-Object System.Management.Automation.PSCredential(
$AdminUsername,
(ConvertTo-SecureString $AdminPassword -AsPlainText -Force)
)
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force
}
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion)
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
function Chk {
param([string]$Name, [scriptblock]$Test)
try { $ok = [bool](& $Test); $err = '' }
catch { $ok = $false; $err = $_.Exception.Message }
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
}
# ════════════════════════════════════════════════════════════════════
# DEPLOY checks (same as Validate-DeployState.ps1)
# ════════════════════════════════════════════════════════════════════
# Firewall
foreach ($p in Get-NetFirewallProfile) {
$n = $p.Name; $e = $p.Enabled
Chk "Firewall $n disabled" { $e -eq $false }
}
# Defender
Chk 'Defender GPO DisableAntiSpyware=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' `
-Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1
}
# WinRM
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
}
Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
}
Chk 'WinRM MaxMemoryPerShellMB >= 2048' {
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048
}
# UAC
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Chk 'UAC EnableLUA=0' {
(Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0
}
Chk 'UAC LocalAccountTokenFilterPolicy=1' {
(Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1
}
# Explorer
Chk 'Explorer LaunchTo=1 (HKCU)' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
-Name LaunchTo -EA Stop).LaunchTo -eq 1
}
# UX hardening
Chk 'NoLockScreen=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' `
-Name NoLockScreen -EA Stop).NoLockScreen -eq 1
}
Chk 'Server Manager policy DoNotOpenAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' `
-Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1
}
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'Server Manager task Disabled' {
$t = Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -EA SilentlyContinue
(-not $t) -or ($t.State -eq 'Disabled')
}
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'DisableCAD=1 (Policies\System)' {
(Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1
}
Chk 'OOBE DisablePrivacyExperience=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' `
-Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1
}
Chk 'DataCollection AllowTelemetry=0' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' `
-Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0
}
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' {
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
}
# ════════════════════════════════════════════════════════════════════
# SETUP checks (post-Prepare state)
# ════════════════════════════════════════════════════════════════════
# CI build user
Chk "user $BuildUsername exists" {
$null -ne (Get-LocalUser -Name $BuildUsername -EA Stop)
}
Chk "user $BuildUsername enabled" {
(Get-LocalUser -Name $BuildUsername -EA Stop).Enabled
}
Chk "user $BuildUsername PasswordNeverExpires" {
(Get-LocalUser -Name $BuildUsername -EA Stop).PasswordExpires -eq $null
}
Chk "user $BuildUsername member of Administrators" {
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
}
# CI directories
foreach ($dir in 'C:\CI\build','C:\CI\output','C:\CI\scripts') {
Chk "$dir exists" { Test-Path $dir -PathType Container }
}
# Windows Update permanently disabled
Chk 'wuauserv StartType=Disabled' { (Get-Service wuauserv -EA Stop).StartType -eq 'Disabled' }
Chk 'UsoSvc StartType=Disabled' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Disabled' }
Chk 'WU GPO NoAutoUpdate=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' `
-Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1
}
Chk 'WU GPO DisableWindowsUpdateAccess=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' `
-Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1
}
# Toolchain
Chk ".NET SDK channel $DotNetChannel" {
$v = (& dotnet --version 2>&1) -as [string]
$v -and $v.StartsWith($DotNetChannel)
}
Chk 'Python C:\Python\python.exe present' { Test-Path 'C:\Python\python.exe' }
Chk "Python version $PythonVersion" {
$v = (& 'C:\Python\python.exe' --version 2>&1) -as [string]
$v -and $v.Trim() -eq "Python $PythonVersion"
}
Chk 'MSBuild.exe present' {
$msb = Get-Command msbuild -ErrorAction SilentlyContinue
$msb -and (Test-Path $msb.Source)
}
# No leftover installer files in C:\CI root
Chk 'no leftover installer scripts in C:\CI' {
$leftovers = Get-ChildItem 'C:\CI' -File -EA SilentlyContinue |
Where-Object { $_.Name -match 'wu_worker|wu_result|vs_buildtools|dotnet-install' }
$leftovers.Count -eq 0
}
return $results.ToArray()
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion
Write-Host ''
$failed = 0
$deployCnt = 0
$setupCnt = 0
$inSetup = $false
foreach ($r in $checks) {
if (-not $inSetup -and $r.Name -match "^user $BuildUsername|^C:\\CI\\|wuauserv Start|UsoSvc Start|WU GPO|\.NET SDK|Python|MSBuild|leftover") {
Write-Host "`n --- Setup checks ---" -ForegroundColor DarkCyan
$inSetup = $true
}
if ($r.Pass) {
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
} else {
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
$failed++
}
}
Write-Host ''
if ($failed -eq 0) {
Write-Host "All $($checks.Count) checks passed — VM ready for BaseClean snapshot." -ForegroundColor Green
exit 0
}
if (-not $Remediate) {
Write-Host "$failed / $($checks.Count) checks FAILED." -ForegroundColor Red
Write-Host "Re-run with -Remediate to auto-fix known issues." -ForegroundColor Yellow
exit 1
}
# ── Remediation pass ─────────────────────────────────────────────────────
$failedNames = @($checks | Where-Object { -not $_.Pass } | Select-Object -ExpandProperty Name)
Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow
Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
param([string[]] $FailedNames, [string] $AdminPassword)
if ($FailedNames -contains 'AutoAdminLogon=1, DefaultUserName=Administrator') {
$wl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
Set-ItemProperty $wl AutoAdminLogon '1' -Type String
Set-ItemProperty $wl DefaultUserName 'Administrator' -Type String
Set-ItemProperty $wl DefaultDomainName '.' -Type String
Set-ItemProperty $wl DefaultPassword $AdminPassword -Type String
Write-Host " [FIX] AutoAdminLogon keys written."
}
if ($FailedNames -contains 'wuauserv StartType=Disabled') {
Set-Service -Name wuauserv -StartupType Disabled -ErrorAction SilentlyContinue
Write-Host " [FIX] wuauserv set to Disabled."
}
if ($FailedNames -contains 'UsoSvc StartType=Disabled') {
Set-Service -Name UsoSvc -StartupType Disabled -ErrorAction SilentlyContinue
Write-Host " [FIX] UsoSvc set to Disabled."
}
} -ArgumentList $failedNames, $AdminPassword
# ── Re-run checks ─────────────────────────────────────────────────────────
Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion)
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
function Chk {
param([string]$Name, [scriptblock]$Test)
try { $ok = [bool](& $Test); $err = '' }
catch { $ok = $false; $err = $_.Exception.Message }
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
}
foreach ($p in Get-NetFirewallProfile) { $n=$p.Name;$e=$p.Enabled; Chk "Firewall $n disabled" { $e -eq $false } }
Chk 'Defender GPO DisableAntiSpyware=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' -Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1 }
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' }
Chk 'WinRM Auth/Basic=true' { (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' }
Chk 'WinRM MaxMemoryPerShellMB >= 2048' { [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048 }
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Chk 'UAC EnableLUA=0' { (Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0 }
Chk 'UAC LocalAccountTokenFilterPolicy=1' { (Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1 }
Chk 'Explorer LaunchTo=1 (HKCU)' { (Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name LaunchTo -EA Stop).LaunchTo -eq 1 }
Chk 'NoLockScreen=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' -Name NoLockScreen -EA Stop).NoLockScreen -eq 1 }
Chk 'Server Manager policy DoNotOpenAtLogon=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' -Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1 }
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1 }
Chk 'Server Manager task Disabled' { $t=Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' -TaskName 'ServerManager' -EA SilentlyContinue; (-not $t) -or ($t.State -eq 'Disabled') }
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' { (Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' -Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1 }
Chk 'DisableCAD=1 (Policies\System)' { (Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1 }
Chk 'OOBE DisablePrivacyExperience=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' -Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1 }
Chk 'DataCollection AllowTelemetry=0' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' -Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0 }
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' { $wl=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop; $wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator' }
Chk 'wuauserv StartType=Disabled' { (Get-Service wuauserv -EA Stop).StartType -eq 'Disabled' }
Chk 'UsoSvc StartType=Disabled' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Disabled' }
Chk 'WU GPO NoAutoUpdate=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1 }
Chk 'WU GPO DisableWindowsUpdateAccess=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1 }
Chk "user $BuildUsername exists" { $null -ne (Get-LocalUser -Name $BuildUsername -EA Stop) }
Chk "user $BuildUsername enabled" { (Get-LocalUser -Name $BuildUsername -EA Stop).Enabled }
Chk "user $BuildUsername PasswordNeverExpires" { (Get-LocalUser -Name $BuildUsername -EA Stop).PasswordExpires -eq $null }
Chk "user $BuildUsername member of Administrators" { [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername) }
foreach ($dir in 'C:\CI\build','C:\CI\output','C:\CI\scripts') { Chk "$dir exists" { Test-Path $dir -PathType Container } }
Chk ".NET SDK channel $DotNetChannel" { $v=(& dotnet --version 2>&1) -as [string]; $v -and $v.StartsWith($DotNetChannel) }
Chk 'Python C:\Python\python.exe present' { Test-Path 'C:\Python\python.exe' }
Chk "Python version $PythonVersion" { $v=(& 'C:\Python\python.exe' --version 2>&1) -as [string]; $v -and $v.Trim() -eq "Python $PythonVersion" }
Chk 'MSBuild.exe present' { $msb=Get-Command msbuild -EA SilentlyContinue; $msb -and (Test-Path $msb.Source) }
Chk 'no leftover installer scripts in C:\CI' { $l=Get-ChildItem 'C:\CI' -File -EA SilentlyContinue | Where-Object { $_.Name -match 'wu_worker|wu_result|vs_buildtools|dotnet-install' }; $l.Count -eq 0 }
return $results.ToArray()
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion
Write-Host ''
$failed = 0
$inSetup = $false
foreach ($r in $checks) {
if (-not $inSetup -and $r.Name -match "^user $BuildUsername|^C:\\CI\\|wuauserv Start|UsoSvc Start|WU GPO|\.NET SDK|Python|MSBuild|leftover") {
Write-Host "`n --- Setup checks ---" -ForegroundColor DarkCyan
$inSetup = $true
}
if ($r.Pass) {
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
} else {
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
$failed++
}
}
Write-Host ''
if ($failed -eq 0) {
Write-Host "All $($checks.Count) checks passed — VM ready for BaseClean snapshot." -ForegroundColor Green
exit 0
} else {
Write-Host "$failed / $($checks.Count) checks still FAILED after remediation." -ForegroundColor Red
exit 1
}
} finally {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
}