7 Commits

Author SHA1 Message Date
Simone d4e619ddc2 feat(template): add WinBuild2022 script variants (Deploy/Prepare/Setup) 2026-05-10 18:17:47 +02:00
Simone 8b54ca01b3 feat(setup): Step 3b — Windows KMS activation via slmgr /skms + /ato
Activates Windows against kms8.msguides.com before Windows Update runs
so WU sees correct license state. Uses cscript //NoLogo to route slmgr
output to stdout (dialogs hang in WinRM sessions). Best-effort: activation
failure emits a warning but does not abort Setup — CI builds function
within the grace period.

Placed between Step 3 (WinRM/network confirmed) and Step 4 (user creation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:27:23 +02:00
Simone d63611f78e fix(setup): $sdFiles.Count null-dereference under StrictMode — wrap in @()
$sdFiles is $null when SoftwareDistribution\Download is empty. StrictMode
throws on $null.Count. @($sdFiles).Count safely returns 0 for null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:20:26 +02:00
Simone ee7f406f2a fix(setup): Measure-Object .Sum null-dereference under Set-StrictMode -Version Latest
PS 5.1 treats GenericMeasureInfo.Sum as absent (not null) when the input pipe
is empty. StrictMode throws 'property Sum cannot be found'. Replace Measure-Object
with an explicit foreach loop for SoftwareDistribution size reporting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:17:41 +02:00
Simone edce871644 fix(setup): defeat WaaSMedicSvc wuauserv healing + AutoAdminLogon Final gate
Two persistent Validate-SetupState failures despite §7.4 fixes:

1. wuauserv — Setup's Assert-Steps pass at exit but WaaSMedicSvc (tamper-protected,
   cannot be disabled) polls SCM ~30-60s and resets StartType to Manual.
   Fix: write Start=4 directly to registry key (SCM API alone is overrideable);
   deny WaaSMedicSvc write access to wuauserv registry key via ACL (best-effort,
   non-fatal if WinRM session lacks permission); re-enforce Start=4 after DISM
   in Cleanup; add Pre-Final re-affirmation block before Final gate.

2. AutoAdminLogon — Step 5c re-affirms it mid-script, but Steps 7-10 take hours
   and no Final gate check catches a late reset. Fix: add Pre-Final re-affirmation
   block (same as wuauserv) + add Assert-Step 'Final' 'AutoAdminLogon=1' to Final gate.

Also update WinISO default path to April 2026 refresh ISO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:56:28 +02:00
Simone 34b33c5011 fix(winrm-https): PS 5.1 Test-WSMan has no -SessionOption — remove from all call sites
Test-WSMan in Windows PowerShell 5.1 does not support -SessionOption (WSManSessionOption),
causing "Cannot find a parameter matching the name 'SessionOption'" at runtime.

Prepare-WinBuild2025.ps1:
  - Remove Test-WSMan connectivity check step entirely
  - Open New-PSSession directly (handles -SkipCACheck via PSSessionOption)
  - Wrap New-PSSession in try/catch with the same actionable error message
  - Remove $wsmOpt (WSManSessionOption) — no longer needed
  Wait-GuestWinRMReady: replace Test-WSMan probe with New-PSSession probe + Remove-PSSession

Wait-VMReady.ps1:
  - Remove $wsmOpt entirely
  - Replace Test-WSMan -UseSSL -SessionOption with Test-NetConnection -Port 5986
    (TCP open on 5986 = HTTPS listener up = VM ready; credentials not available here)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:58:57 +02:00
Simone 68cde01c9d security(sprint1): §1.1/1.3/1.4 — WinRM HTTPS/5986, SHA256 pinning infra, IP regex per-octet
§1.4 — Replace loose IP ValidatePattern with per-octet regex in 4 scripts
  (Invoke-CIJob, Invoke-RemoteBuild, Get-BuildArtifacts, Wait-VMReady)

§1.1 — Migrate all WinRM connections from HTTP/5985 to HTTPS/5986
  - Deploy post-install.ps1: remove AllowUnencrypted=true; self-signed cert + HTTPS listener
    already present; firewall rule restricted to 192.168.79.0/24
  - Setup-WinBuild2025.ps1: assert AllowUnencrypted=false
  - Prepare-WinBuild2025.ps1: preflight uses TCP/5986; Test-WSMan -UseSSL -Port 5986;
    New-PSSession -UseSSL -Port 5986; host AllowUnencrypted save/restore removed (not needed for HTTPS)
  - Invoke-RemoteBuild, Get-BuildArtifacts: New-PSSession -UseSSL -Port 5986 -Authentication Basic
  - Wait-VMReady: New-WSManSessionOption + Test-WSMan -Port 5986 -UseSSL
  - Validate-DeployState, Validate-SetupState: Invoke-Command -UseSSL -Port 5986,
    AllowUnencrypted check → false; AllowUnencrypted host-side removed from both
  - Docs: PLAN, README, ARCHITECTURE, BEST-PRACTICES, HOST-SETUP, WINDOWS-TEMPLATE-SETUP,
    LINUX-TEMPLATE-SETUP, TODO updated

§1.3 — SHA256 hash pinning infrastructure
  - Assert-Hash function + $script:Hashes hashtable added to Setup-WinBuild2025.ps1
  - Assert-Hash called after dotnet-install.ps1, python installer, vs_buildtools.exe downloads
  - Hashes initialized to '' (warn-skip); must be filled before next snapshot refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 14:46:41 +02:00
20 changed files with 3289 additions and 241 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
- Gitea self-hosted — `http://10.10.20.11:3100` - Gitea self-hosted — `http://10.10.20.11:3100`
- act_runner v1.0.2 — Windows service su host, label `windows-build` - act_runner v1.0.2 — Windows service su host, label `windows-build`
- Build VMs: subnet VMnet8 NAT — `192.168.79.0/24` - Build VMs: subnet VMnet8 NAT — `192.168.79.0/24`
- WinRM HTTP/5985 (Basic auth, lab-only) - WinRM HTTPS/5986 (Basic auth over HTTPS, self-signed cert, SkipCACheck lab-only)
### Architettura implementata ### Architettura implementata
1. Gitea Actions enqueue job → act_runner lo prende 1. Gitea Actions enqueue job → act_runner lo prende
+5 -4
View File
@@ -54,7 +54,7 @@ Ogni build gira in una VM Windows Server 2025 **usa e getta**, ripristinata dall
| .NET SDK | 10.0.203 | | .NET SDK | 10.0.203 |
| Python | 3.13.3 | | Python | 3.13.3 |
| Rete | VMnet8 NAT — `192.168.79.0/24` | | Rete | VMnet8 NAT — `192.168.79.0/24` |
| WinRM | HTTP/5985, Basic auth (lab-only) | | WinRM | HTTPS/5986, Basic auth, self-signed cert (SkipCACheck — lab-only) |
--- ---
@@ -181,6 +181,7 @@ e aggiorna `-RepoUrl`, `-BuildCommand` e `-GuestArtifactSource`.
## Note di sicurezza ## Note di sicurezza
WinRM in modalità HTTP/Basic è **accettabile solo in ambiente lab isolato**. WinRM usa **HTTPS/5986** con certificato self-signed e Basic auth.
Per ambienti di produzione o condivisi, migrare a HTTPS/5986 con certificato self-signed `-SkipCACheck`/`-SkipCNCheck` sono accettabili in lab isolato (il cert non è di una CA trusted).
e rimuovere `AllowUnencrypted=true`. Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md). Per ambienti condivisi o di produzione, usare un certificato CA valida e rimuovere `-SkipCACheck`.
Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md).
+97 -76
View File
@@ -16,7 +16,35 @@
> - **P3** — estensioni nice-to-have > - **P3** — estensioni nice-to-have
--- ---
## Summary
_Last updated: 2026-05-10 (post §7.4 e2e + tag v1.2)_
**Stato generale**: infrastruttura base e refactor Deploy↔Setup (§7) completi. Sicurezza sprint 1 in corso (HTTPS/5986 done, IP regex done, hash pinning struttura done — valori da riempire). Backlog operatività/perf/qualità intatto.
| Area | Done | Open | Note |
| ------------------------------- | ---: | ---: | ----------------------------------------------------------------- |
| Setup & Infrastructure | all | 0 | Gitea + runner + dirs + rete tutti operativi |
| Template VM (Fasi A→D) | all | 0 | Snapshot `BaseClean` preso, credenziali in Credential Manager |
| Scripts Verification | all | 0 | e2e-008/009 SUCCESS, cleanup orfani + retry deleteVM ok |
| Runner Configuration | all | 0 | `config.yaml`, capacity 4, `BuildVMGuest` |
| Gitea Workflow Integration | 4 | 1 | manca solo §P3 generalizzazione workflow |
| §1 Sicurezza & hardening | 2 | 6 | 1.1, 1.4 done; 1.2/1.3 parzialmente; 1.5/1.6/1.7/1.8 da fare |
| §2 Affidabilità & resilienza | 0 | 7 | tutto backlog (race IP, cleanup schedulato, snapshot versionato…) |
| §3 Performance & throughput | 0 | 6 | nessuna ottimizzazione applicata; baseline §3.6 prerequisito |
| §4 Osservabilità & manutenzione | 0 | 5 | log JSONL, metriche, runbook tutti aperti |
| §5 Qualità codice & test | 0 | 5 | Pester, modulo `_Common.psm1`, regole PSScriptAnalyzer custom |
| §6 Scalabilità & estensioni | 0 | 6 | Linux VM, composite action, toolchain Tier-1 |
| §7 Refactor Deploy ↔ Setup | 4 | 1 | 7.1/7.2/7.3/7.4 done; 7.5 (rinomina) opzionale |
| In-VM Git Clone (§3.3) | 0 | 4 | dipende da §6.6 (Git+7-Zip nel template) |
| Linux Build VM (§6.1) | 0 | 4 | bozza in `docs/LINUX-TEMPLATE-SETUP.md` |
**Prossimi passi consigliati** (vedi anche "Suggerimento di sequencing" in fondo):
1. Chiudere §1.3 — riempire hash SHA256 in `Setup-WinBuild2025.ps1` prima del prossimo refresh snapshot.
2. Audit §1.2 (TrustedHosts `*` su host), poi §1.7 (rotazione password guest).
3. Sprint 2 operatività — §2.2 (cleanup schedulato), §2.3 (retention), §2.5 (snapshot versionato).
---
## Setup & Infrastructure ## Setup & Infrastructure
- [x] **Gitea Server** — già in esecuzione su `http://10.10.20.11:3100` (e `https://gitea.emulab.it`) - [x] **Gitea Server** — già in esecuzione su `http://10.10.20.11:3100` (e `https://gitea.emulab.it`)
@@ -65,7 +93,7 @@
```powershell ```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force
Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force # AllowUnencrypted left false — Deploy post-install.ps1 configures HTTPS/5986 listener
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
``` ```
- [x] **Attiva Windows Server 2025** (prima dello snapshot — i linked clone ereditano l'attivazione): - [x] **Attiva Windows Server 2025** (prima dello snapshot — i linked clone ereditano l'attivazione):
@@ -87,7 +115,7 @@
``` ```
Installa: WinRM config, utente `ci_build`, .NET SDK 10.0, VS Build Tools 2026, Python 3.13.3. Installa: WinRM config, utente `ci_build`, .NET SDK 10.0, VS Build Tools 2026, Python 3.13.3.
Lo script esegue validazione automatica ad ogni passaggio (`Assert-Step`): Lo script esegue validazione automatica ad ogni passaggio (`Assert-Step`):
- Pre-flight: IP ottetti 0-255, TCP/5985 raggiungibile, Setup-WinBuild2025.ps1 presente - Pre-flight: IP ottetti 0-255, TCP/5986 raggiungibile, Setup-WinBuild2025.ps1 presente
- Host WinRM: AllowUnencrypted, TrustedHosts - Host WinRM: AllowUnencrypted, TrustedHosts
- Guest prep: `C:\CI` esiste, file copiato, size match - Guest prep: `C:\CI` esiste, file copiato, size match
- Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs - Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs
@@ -148,25 +176,22 @@
## 1. Sicurezza & hardening ## 1. Sicurezza & hardening
### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 ### 1.1 [P0] [x] Migrare WinRM da HTTP/Basic a HTTPS/5986 — COMPLETATO 2026-05-10
File: [template/Setup-WinBuild2025.ps1:204-241](template/Setup-WinBuild2025.ps1) (Step 3 WinRM), [template/Deploy-WinBuild2025.ps1:524-534](template/Deploy-WinBuild2025.ps1) (Enable-PSRemoting + AllowUnencrypted/Basic), [scripts/Invoke-RemoteBuild.ps1:83-91](scripts/Invoke-RemoteBuild.ps1), [scripts/Get-BuildArtifacts.ps1:66-75](scripts/Get-BuildArtifacts.ps1).
**Motivazione**: la combo `AllowUnencrypted=true` + `Auth/Basic=true` invia credenziali del
`ci_build` (admin) in chiaro su VMnet8. Anche in lab isolato, l'host può essere compromesso
via altri vettori e scrapare la rete VMware. Il piano è già in `docs/BEST-PRACTICES.md §2`;
manca solo l'esecuzione.
**Azioni**: **Azioni**:
- [ ] Generare cert self-signed nel template *prima* dello snapshot. - [x] Generare cert self-signed nel template *prima* dello snapshot (già in Deploy post-install.ps1).
- [ ] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986` in `Invoke-RemoteBuild.ps1` e `Get-BuildArtifacts.ps1`. - [x] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986 -Authentication Basic` in `Invoke-RemoteBuild.ps1`, `Get-BuildArtifacts.ps1`, `Prepare-WinBuild2025.ps1`, `Validate-*.ps1`.
- [ ] Rimuovere `AllowUnencrypted=true` dal `Setup-WinBuild2025.ps1`. - [x] Rimuovere `AllowUnencrypted=true` da Deploy post-install.ps1 e Setup-WinBuild2025.ps1 (assertion → false).
- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline. - [x] `-SkipCACheck`/`-SkipCNCheck`/`-SkipRevocationCheck` mantenuti nei `New-PSSessionOption` e `New-WSManSessionOption` (cert lab self-signed — documentato inline).
- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT). - [x] Regola firewall `WinRM-HTTPS` in Deploy ristretta a `RemoteAddress '192.168.79.0/24'`.
- [x] `Wait-VMReady.ps1` usa `New-WSManSessionOption` + `Test-WSMan -Port 5986 -UseSSL`.
- [x] `Prepare-WinBuild2025.ps1`: preflight TCP/5986, host non imposta più `AllowUnencrypted`; solo `TrustedHosts` viene salvato/ripristinato.
- [x] `Validate-DeployState.ps1`, `Validate-SetupState.ps1`: connessione HTTPS/5986, check `AllowUnencrypted=false`.
### 1.2 [P0] Restringere `TrustedHosts` lato host (audit-only — già coperto) ### 1.2 [P0] [ ] Restringere `TrustedHosts` lato host (audit-only — residuo: audit host pre-esistenti)
File: nessuno script imposta `*` sull'host. Stato corrente: File: nessuno script imposta `*` sull'host. Stato corrente:
- `Setup-Host.ps1` non tocca TrustedHosts (verificato 2026-05-10) - `Setup-Host.ps1` non tocca TrustedHosts (verificato 2026-05-10)
- [template/Prepare-WinBuild2025.ps1:270-294](template/Prepare-WinBuild2025.ps1:270) appende `$VMIPAddress` e ripristina nel `finally` (riga 617) - [template/Prepare-WinBuild2025.ps1](template/Prepare-WinBuild2025.ps1) appende `$VMIPAddress` ai `TrustedHosts` e ripristina nel `finally` (post-§1.1: solo TrustedHosts, no AllowUnencrypted)
- [docs/HOST-SETUP.md:121-126](docs/HOST-SETUP.md:121) raccomanda `'192.168.79.*'` in setup manuale - [docs/HOST-SETUP.md:121-126](docs/HOST-SETUP.md:121) raccomanda `'192.168.79.*'` in setup manuale
**Azione residua**: audit eventuali host già configurati con `*` (eredità manuale) e **Azione residua**: audit eventuali host già configurati con `*` (eredità manuale) e
@@ -175,34 +200,30 @@ sostituire con la subnet build:
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
``` ```
### 1.3 [P0] Pinning hash SHA256 degli installer ### 1.3 [P0] [ ] Pinning hash SHA256 degli installer — STRUTTURA AGGIUNTA 2026-05-10 (valori da riempire)
File: [template/Setup-WinBuild2025.ps1:707-755](template/Setup-WinBuild2025.ps1) (Step 8 Python), [template/Setup-WinBuild2025.ps1:757-885](template/Setup-WinBuild2025.ps1) (Step 9 VS Build Tools), [template/Setup-WinBuild2025.ps1:660-705](template/Setup-WinBuild2025.ps1) (Step 7 dotnet-install.ps1).
**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza `$script:Hashes` + `Assert-Hash` aggiunti a [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari `Assert-Hash` viene chiamato dopo ogni download (dotnet-install.ps1, python installer, vs_buildtools.exe).
compromessi nel template, che poi viene snapshottato e usato da *ogni* build. Se l'hash è `''` viene emesso un warning ma l'esecuzione continua (non-breaking).
Pattern minimale: **Azione residua — OBBLIGATORIA prima del prossimo refresh snapshot**:
```powershell - [ ] Scaricare i tre file, calcolare hash, riempire `$script:Hashes` in Setup-WinBuild2025.ps1:
$expected = '6F25A7DF...' # SHA256 pinnato per la versione esatta ```powershell
$actual = (Get-FileHash $pyInstallerPath -Algorithm SHA256).Hash.ToLower() # Inside or outside VM — qualsiasi PowerShell:
if ($actual -ne $expected.ToLower()) { throw "Hash mismatch: $actual" } (Get-FileHash 'python-3.13.3-amd64.exe' -Algorithm SHA256).Hash # → $script:Hashes['Python']
``` (Get-FileHash 'dotnet-install.ps1' -Algorithm SHA256).Hash # → $script:Hashes['DotNetInstallScript']
Aggiungere un blocco hash per ogni download e bloccarli in costanti `$script:Hashes`. (Get-FileHash 'vs_buildtools.exe' -Algorithm SHA256).Hash # → $script:Hashes['VSBuildToolsBootstrapper']
Aggiornare al cambio versione (è raro). ```
- [ ] Verificare che `Assert-Hash` throwi correttamente su hash errato (test con hash fake).
### 1.4 [P1] Validazione IP per-ottetto ### 1.4 [P1] [x] Validazione IP per-ottetto — COMPLETATO 2026-05-10
File: [scripts/Invoke-CIJob.ps1:106](scripts/Invoke-CIJob.ps1), [scripts/Invoke-RemoteBuild.ps1:52](scripts/Invoke-RemoteBuild.ps1), [scripts/Get-BuildArtifacts.ps1:42](scripts/Get-BuildArtifacts.ps1), [scripts/Wait-VMReady.ps1:43](scripts/Wait-VMReady.ps1).
**Motivazione**: `'^(\d{1,3}\.){3}\d{1,3}$'` accetta `999.999.999.999`. Sostituire con regex Regex per-ottetto sostituita in tutti e 4 i file:
per-ottetto: `'^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$'`
```powershell
$ipv4 = '^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$'
[ValidatePattern($ipv4)]
```
Estrarre la costante in un modulo condiviso `scripts/_Common.psm1` per evitare drift fra i 4 file (vedi §5.2).
### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone` Estrazione in `scripts/_Common.psm1` — vedi §5.2 (P2, backlog).
### 1.5 [P1] [ ] PAT mai persistito quando si abilita `-UseGitClone`
Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa
(quando, dove, come) è dettagliata nella sezione "In-VM Git Clone (Ottimizzazione) — riferimento §3.3". (quando, dove, come) è dettagliata nella sezione "In-VM Git Clone (Ottimizzazione) — riferimento §3.3".
@@ -214,7 +235,7 @@ Vincoli da rispettare in qualsiasi implementazione di `-UseGitClone`:
`transcript.txt`, marcare la build come fallita e ruotare il PAT (regola di safety net, `transcript.txt`, marcare la build come fallita e ruotare il PAT (regola di safety net,
non sostituisce le precedenti). non sostituisce le precedenti).
### 1.6 [P2] Defender + Firewall + UAC tutti disattivati nel template — documentare il modello di minaccia ### 1.6 [P2] [ ] Defender + Firewall + UAC tutti disattivati nel template — documentare il modello di minaccia
File: [template/Deploy-WinBuild2025.ps1:485-551](template/Deploy-WinBuild2025.ps1) (UAC, ServerManager, DisableCAD, OOBE — set da Deploy), [template/Setup-WinBuild2025.ps1:176-336](template/Setup-WinBuild2025.ps1) (Firewall Step 1, Defender Step 2 validation-only post-refactor 2026-05-09, WinRM Step 3, User Step 4, UAC Step 4b, Dirs Step 5). File: [template/Deploy-WinBuild2025.ps1:485-551](template/Deploy-WinBuild2025.ps1) (UAC, ServerManager, DisableCAD, OOBE — set da Deploy), [template/Setup-WinBuild2025.ps1:176-336](template/Setup-WinBuild2025.ps1) (Firewall Step 1, Defender Step 2 validation-only post-refactor 2026-05-09, WinRM Step 3, User Step 4, UAC Step 4b, Dirs Step 5).
Stato attuale (post-refactor §7): Stato attuale (post-refactor §7):
@@ -233,11 +254,11 @@ Le scelte sono ragionevoli per un template lab efimero, ma vanno raccolte in un'
Senza questa nota, future modifiche possono accumulare ulteriori riduzioni di sicurezza Senza questa nota, future modifiche possono accumulare ulteriori riduzioni di sicurezza
senza tracciamento. senza tracciamento.
### 1.7 [P2] Rotazione password guest VM ### 1.7 [P2] [ ] Rotazione password guest VM
- [ ] Trimestrale. Aggiornare credenziale `BuildVMGuest` in Credential Manager + password - [ ] Trimestrale. Aggiornare credenziale `BuildVMGuest` in Credential Manager + password
effettiva nel template (richiede refresh snapshot — coordinare con §2.5). effettiva nel template (richiede refresh snapshot — coordinare con §2.5).
### 1.8 [P2] Verifica Get-StoredCredential ### 1.8 [P2] [ ] Verifica Get-StoredCredential
- [ ] Confermare che `Get-StoredCredential` funzioni correttamente in tutti gli script che - [ ] Confermare che `Get-StoredCredential` funzioni correttamente in tutti gli script che
lo usano (post-migrazione HTTPS). lo usano (post-migrazione HTTPS).
@@ -245,7 +266,7 @@ senza tracciamento.
## 2. Affidabilità & resilienza ## 2. Affidabilità & resilienza
### 2.1 [P0] Race su IP allocation con `capacity: 4` ### 2.1 [P0] [ ] Race su IP allocation con `capacity: 4`
File: [runner/config.yaml:17](runner/config.yaml), [scripts/Invoke-CIJob.ps1:244-253](scripts/Invoke-CIJob.ps1). File: [runner/config.yaml:17](runner/config.yaml), [scripts/Invoke-CIJob.ps1:244-253](scripts/Invoke-CIJob.ps1).
**Stato osservato**: race non riprodotta in e2e-008/009 (un job alla volta). `capacity: 4` **Stato osservato**: race non riprodotta in e2e-008/009 (un job alla volta). `capacity: 4`
@@ -266,7 +287,7 @@ rilevata in fase di Wait-VMReady — apparirà come "WinRM risponde ma è la VM
L'opzione A è la più portabile e si integra con il pre-warm pool (§3.4). L'opzione A è la più portabile e si integra con il pre-warm pool (§3.4).
### 2.2 [P1] Cleanup orfani schedulato ### 2.2 [P1] [ ] Cleanup orfani schedulato
File: [scripts/Cleanup-OrphanedBuildVMs.ps1](scripts/Cleanup-OrphanedBuildVMs.ps1) — script pronto, manca lo scheduling. File: [scripts/Cleanup-OrphanedBuildVMs.ps1](scripts/Cleanup-OrphanedBuildVMs.ps1) — script pronto, manca lo scheduling.
Estendere `Setup-Host.ps1` per registrare un Task Scheduler: Estendere `Setup-Host.ps1` per registrare un Task Scheduler:
@@ -280,7 +301,7 @@ Register-ScheduledTask -TaskName 'CI-CleanupOrphans' -Action $action -Trigger $t
Aggiungere anche un trigger `AtStartup` per gestire crash dell'host. Aggiungere anche un trigger `AtStartup` per gestire crash dell'host.
Suggerito: ogni 6 ore, `-MaxAgeHours 4`. Suggerito: ogni 6 ore, `-MaxAgeHours 4`.
### 2.3 [P1] Retention artifact + log automatica ### 2.3 [P1] [ ] Retention artifact + log automatica
File: [docs/OPTIMIZATION.md:160-181](docs/OPTIMIZATION.md) — la logica c'è solo come snippet, non eseguita. File: [docs/OPTIMIZATION.md:160-181](docs/OPTIMIZATION.md) — la logica c'è solo come snippet, non eseguita.
Stessa pattern di §2.2 ma su `F:\CI\Artifacts` con `-AddDays(-30)` e su `F:\CI\Logs` con la Stessa pattern di §2.2 ma su `F:\CI\Artifacts` con `-AddDays(-30)` e su `F:\CI\Logs` con la
@@ -288,13 +309,13 @@ finestra già configurata in `Invoke-CIJob.ps1`. Aggiungere
`Get-PSDrive F | Where-Object { $_.Free -lt 50GB }` come *guard*: se libero < 50 GB, `Get-PSDrive F | Where-Object { $_.Free -lt 50GB }` come *guard*: se libero < 50 GB,
retention più aggressiva (7 giorni) e log warning. retention più aggressiva (7 giorni) e log warning.
### 2.4 [P1] `Remove-BuildVM.ps1` — supportare `-WhatIf` ### 2.4 [P1] [ ] `Remove-BuildVM.ps1` — supportare `-WhatIf`
File: [scripts/Remove-BuildVM.ps1:28](scripts/Remove-BuildVM.ps1). File: [scripts/Remove-BuildVM.ps1:28](scripts/Remove-BuildVM.ps1).
Aggiungere `[CmdletBinding(SupportsShouldProcess)]` come fatto in `Cleanup-OrphanedBuildVMs.ps1`. Aggiungere `[CmdletBinding(SupportsShouldProcess)]` come fatto in `Cleanup-OrphanedBuildVMs.ps1`.
Permette debug e dry-run senza forkare un secondo script. Permette debug e dry-run senza forkare un secondo script.
### 2.5 [P1] Snapshot versionato `BaseClean_<yyyyMMdd>` ### 2.5 [P1] [ ] Snapshot versionato `BaseClean_<yyyyMMdd>`
File: [docs/BEST-PRACTICES.md:142-145](docs/BEST-PRACTICES.md), [scripts/Invoke-CIJob.ps1:100](scripts/Invoke-CIJob.ps1). File: [docs/BEST-PRACTICES.md:142-145](docs/BEST-PRACTICES.md), [scripts/Invoke-CIJob.ps1:100](scripts/Invoke-CIJob.ps1).
**Motivazione**: quando si refresha il template (KMS lease semestrale, update toolchain), il **Motivazione**: quando si refresha il template (KMS lease semestrale, update toolchain), il
@@ -310,13 +331,13 @@ e non c'è rollback.
Si lega al task: refresh semestrale snapshot template VM (KMS lease = 180 giorni). Si lega al task: refresh semestrale snapshot template VM (KMS lease = 180 giorni).
Boot template su VMnet8 (NAT) → `slmgr /ato` → nuovo snapshot. Boot template su VMnet8 (NAT) → `slmgr /ato` → nuovo snapshot.
### 2.6 [P2] Backup automatico VMDK template ### 2.6 [P2] [ ] Backup automatico VMDK template
File: [docs/BEST-PRACTICES.md:130-138](docs/BEST-PRACTICES.md) — solo come snippet manuale. File: [docs/BEST-PRACTICES.md:130-138](docs/BEST-PRACTICES.md) — solo come snippet manuale.
Pre-snapshot: copia atomica del `parent` VMDK in `F:\CI\Backups\Template_<date>\`. Su errore Pre-snapshot: copia atomica del `parent` VMDK in `F:\CI\Backups\Template_<date>\`. Su errore
di refresh (es. installer rotto), rollback in 1 minuto. di refresh (es. installer rotto), rollback in 1 minuto.
### 2.7 [P2] Health check del runner + monitoraggio Event Log ### 2.7 [P2] [ ] Health check del runner + monitoraggio Event Log
File: [docs/BEST-PRACTICES.md:101-115](docs/BEST-PRACTICES.md) — script presente in doc, non operativo. File: [docs/BEST-PRACTICES.md:101-115](docs/BEST-PRACTICES.md) — script presente in doc, non operativo.
Schedulare ogni 15 min: query `gitea/api/v1/admin/runners`, se `local-windows-runner` non Schedulare ogni 15 min: query `gitea/api/v1/admin/runners`, se `local-windows-runner` non
@@ -329,7 +350,7 @@ EventLog query schedulata o webhook).
## 3. Performance & throughput ## 3. Performance & throughput
### 3.1 [P1] NuGet/pip cache su shared folder ### 3.1 [P1] [ ] NuGet/pip cache su shared folder
File: [docs/OPTIMIZATION.md:93-128](docs/OPTIMIZATION.md), [scripts/Invoke-RemoteBuild.ps1:158-194](scripts/Invoke-RemoteBuild.ps1). File: [docs/OPTIMIZATION.md:93-128](docs/OPTIMIZATION.md), [scripts/Invoke-RemoteBuild.ps1:158-194](scripts/Invoke-RemoteBuild.ps1).
**Motivazione**: `F:\CI\Cache\NuGet` esiste come dir, non viene mai usata. **Motivazione**: `F:\CI\Cache\NuGet` esiste come dir, non viene mai usata.
@@ -350,7 +371,7 @@ $env:PIP_CACHE_DIR = '\\vmware-host\Shared Folders\pip-cache'
``` ```
Dopo lo snapshot refresh la cache va riscaldata una volta — accettabile. Dopo lo snapshot refresh la cache va riscaldata una volta — accettabile.
### 3.2 [P1] Sostituire `Compress-Archive` con 7-Zip o robocopy ### 3.2 [P1] [ ] Sostituire `Compress-Archive` con 7-Zip o robocopy
File: [scripts/Invoke-RemoteBuild.ps1:112,148,185](scripts/Invoke-RemoteBuild.ps1) (3 chiamate `Compress-Archive`). File: [scripts/Invoke-RemoteBuild.ps1:112,148,185](scripts/Invoke-RemoteBuild.ps1) (3 chiamate `Compress-Archive`).
Prerequisito: 7-Zip nel template — vedi §6.6 e sezione "In-VM Git Clone". Prerequisito: 7-Zip nel template — vedi §6.6 e sezione "In-VM Git Clone".
@@ -368,12 +389,12 @@ può prendere 20-40 s.
Misurare con un repo reale: probabile risparmio 10-20 s/build su `nsis-plugin-nsinnounp`. Misurare con un repo reale: probabile risparmio 10-20 s/build su `nsis-plugin-nsinnounp`.
### 3.3 [P2] In-VM clone (`-UseGitClone`) ### 3.3 [P2] [ ] In-VM clone (`-UseGitClone`)
Implementazione disegnata di seguito (sezione "In-VM Git Clone (Ottimizzazione)"). Implementazione disegnata di seguito (sezione "In-VM Git Clone (Ottimizzazione)").
Vincoli sicurezza PAT: vedi §1.5. Tool prerequisiti (Git, 7-Zip): vedi §6.6. Vincoli sicurezza PAT: vedi §1.5. Tool prerequisiti (Git, 7-Zip): vedi §6.6.
Beneficio reale solo per repo > 200 MB con submoduli grandi; misurare prima. Beneficio reale solo per repo > 200 MB con submoduli grandi; misurare prima.
### 3.4 [P3] Pre-warm pool di cloni ### 3.4 [P3] [ ] Pre-warm pool di cloni
File: [docs/OPTIMIZATION.md:133-156](docs/OPTIMIZATION.md). File: [docs/OPTIMIZATION.md:133-156](docs/OPTIMIZATION.md).
Solo se profilo dimostra che `New-BuildVM` + `Wait-VMReady` (~30-60 s) è il collo di bottiglia. Solo se profilo dimostra che `New-BuildVM` + `Wait-VMReady` (~30-60 s) è il collo di bottiglia.
@@ -383,7 +404,7 @@ Implementazione minima: scheduled task ogni 5 min mantiene 2 cloni avviati in
`F:\CI\WarmPool\`; `Invoke-CIJob` fa `Move-Item` di un clone caldo nel suo job dir e parte `F:\CI\WarmPool\`; `Invoke-CIJob` fa `Move-Item` di un clone caldo nel suo job dir e parte
da Phase 4 saltando Phase 2-3. da Phase 4 saltando Phase 2-3.
### 3.5 [P2] vCPU/RAM tuning per workload ### 3.5 [P2] [ ] vCPU/RAM tuning per workload
File: [docs/OPTIMIZATION.md:82-89](docs/OPTIMIZATION.md), VMX template. File: [docs/OPTIMIZATION.md:82-89](docs/OPTIMIZATION.md), VMX template.
`numvcpus=4`, `memsize=6144` è ragionevole ma non ottimo per ogni build. Per `numvcpus=4`, `memsize=6144` è ragionevole ma non ottimo per ogni build. Per
@@ -393,14 +414,14 @@ Considerare:
- VMX pinning: 6 vCPU per build C++ pesanti, 2 vCPU per build .NET piccoli. - VMX pinning: 6 vCPU per build C++ pesanti, 2 vCPU per build .NET piccoli.
- Esporre `-VMCpu` / `-VMMemory` a `Invoke-CIJob.ps1` con override via workflow. - Esporre `-VMCpu` / `-VMMemory` a `Invoke-CIJob.ps1` con override via workflow.
### 3.6 [P2] Benchmark baseline ### 3.6 [P2] [ ] Benchmark baseline
- [ ] Tempo creazione clone + tempo WinRM readiness (per stabilire baseline prima di §3.1/3.2/3.4). - [ ] Tempo creazione clone + tempo WinRM readiness (per stabilire baseline prima di §3.1/3.2/3.4).
--- ---
## 4. Osservabilità & manutenzione ## 4. Osservabilità & manutenzione
### 4.1 [P1] Log strutturati per parsing ### 4.1 [P1] [ ] Log strutturati per parsing
File: [scripts/Invoke-CIJob.ps1:178-187](scripts/Invoke-CIJob.ps1). File: [scripts/Invoke-CIJob.ps1:178-187](scripts/Invoke-CIJob.ps1).
**Motivazione**: i log attuali sono `Write-Host` testuale. Per dashboard / alerting servono **Motivazione**: i log attuali sono `Write-Host` testuale. Per dashboard / alerting servono
@@ -423,7 +444,7 @@ function Write-JobEvent {
Emettere in ogni transizione di fase. Permette grafici (durata media per fase, tasso di Emettere in ogni transizione di fase. Permette grafici (durata media per fase, tasso di
fallimento) con `jq` o Loki/Grafana se in futuro. fallimento) con `jq` o Loki/Grafana se in futuro.
### 4.2 [P2] Metriche su Prometheus textfile ### 4.2 [P2] [ ] Metriche su Prometheus textfile
Generare `F:\CI\Metrics\runner.prom` da uno scheduled task ogni 60s: Generare `F:\CI\Metrics\runner.prom` da uno scheduled task ogni 60s:
``` ```
ci_runner_orphan_vms 0 ci_runner_orphan_vms 0
@@ -434,13 +455,13 @@ ci_runner_active_jobs 1
Se l'homelab ha già Prometheus + node_exporter, basta Se l'homelab ha già Prometheus + node_exporter, basta
`--collector.textfile.directory=F:\CI\Metrics`. `--collector.textfile.directory=F:\CI\Metrics`.
### 4.3 [P1] Disk space alert ### 4.3 [P1] [ ] Disk space alert
File: estendere `Setup-Host.ps1` o creare `scripts/Watch-DiskSpace.ps1`. File: estendere `Setup-Host.ps1` o creare `scripts/Watch-DiskSpace.ps1`.
`F:` riempito = build silenziosamente fallite (linked clone fallirà su `vmrun clone` con `F:` riempito = build silenziosamente fallite (linked clone fallirà su `vmrun clone` con
messaggi criptici). Alert via `eventcreate` o webhook Gitea/Discord quando libero < 50 GB. messaggi criptici). Alert via `eventcreate` o webhook Gitea/Discord quando libero < 50 GB.
### 4.4 [P2] Runbook per incident comuni ### 4.4 [P2] [ ] Runbook per incident comuni
File: nuovo `docs/RUNBOOK.md`. File: nuovo `docs/RUNBOOK.md`.
Documentare con copy-pasta: Documentare con copy-pasta:
@@ -451,7 +472,7 @@ Documentare con copy-pasta:
Ogni voce: sintomo, comando di triage, fix, escalation. Ogni voce: sintomo, comando di triage, fix, escalation.
### 4.5 [P3] Dashboard read-only ### 4.5 [P3] [ ] Dashboard read-only
Una pagina HTML statica generata da `Invoke-CIJob.ps1` (append a `F:\CI\dashboard.html`) Una pagina HTML statica generata da `Invoke-CIJob.ps1` (append a `F:\CI\dashboard.html`)
con ultime 50 build, durata, esito, link agli artifact. Servita da IIS Express o solo con ultime 50 build, durata, esito, link agli artifact. Servita da IIS Express o solo
file:// — utile per debug a colpo d'occhio senza aprire Gitea UI. file:// — utile per debug a colpo d'occhio senza aprire Gitea UI.
@@ -460,7 +481,7 @@ file:// — utile per debug a colpo d'occhio senza aprire Gitea UI.
## 5. Qualità codice & test ## 5. Qualità codice & test
### 5.1 [P1] Pester smoke tests sugli script ### 5.1 [P1] [ ] Pester smoke tests sugli script
File: nuovo `tests/` directory. File: nuovo `tests/` directory.
**Motivazione**: manca un livello di test sotto l'e2e. **Motivazione**: manca un livello di test sotto l'e2e.
@@ -474,7 +495,7 @@ Pester può coprire:
Eseguire in CI tramite il runner stesso (workflow `lint.yml` esiste già — espandere). Eseguire in CI tramite il runner stesso (workflow `lint.yml` esiste già — espandere).
### 5.2 [P2] Modulo PowerShell condiviso `scripts/_Common.psm1` ### 5.2 [P2] [ ] Modulo PowerShell condiviso `scripts/_Common.psm1`
**Duplicazioni da estrarre**: **Duplicazioni da estrarre**:
- `New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck` (3 file). - `New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck` (3 file).
- `ValidatePattern` IP (4 file — vedi §1.4). - `ValidatePattern` IP (4 file — vedi §1.4).
@@ -484,7 +505,7 @@ Eseguire in CI tramite il runner stesso (workflow `lint.yml` esiste già — esp
Risultato: meno LoC, fix in un punto solo, più facile da testare. Risultato: meno LoC, fix in un punto solo, più facile da testare.
### 5.3 [P2] Esecuzione `vmrun` con check exit code uniforme ### 5.3 [P2] [ ] Esecuzione `vmrun` con check exit code uniforme
Pattern attuale ripetuto in tutti gli script che invocano `vmrun`: Pattern attuale ripetuto in tutti gli script che invocano `vmrun`:
```powershell ```powershell
$out = & $VmrunPath ... 2>&1 $out = & $VmrunPath ... 2>&1
@@ -493,7 +514,7 @@ if ($LASTEXITCODE -ne 0) { throw "..." }
Wrapping in `Invoke-VmrunCommand -Operation 'clone' -Args @(...)` riduce errori di copy-paste Wrapping in `Invoke-VmrunCommand -Operation 'clone' -Args @(...)` riduce errori di copy-paste
e centralizza retry/log. e centralizza retry/log.
### 5.4 [P2] PSScriptAnalyzer rules custom ### 5.4 [P2] [ ] PSScriptAnalyzer rules custom
File: [gitea/workflows/lint.yml](gitea/workflows/lint.yml) (già esiste). File: [gitea/workflows/lint.yml](gitea/workflows/lint.yml) (già esiste).
Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root): Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
@@ -502,7 +523,7 @@ Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
- `PSUseShouldProcessForStateChangingFunctions`. - `PSUseShouldProcessForStateChangingFunctions`.
- Regole custom: vietare hardcoded `F:\CI\` fuori da config (forzare param/env). - Regole custom: vietare hardcoded `F:\CI\` fuori da config (forzare param/env).
### 5.5 [P3] Type hints nei param block ### 5.5 [P3] [ ] Type hints nei param block
Già parzialmente fatto. Estendere a tutti gli script per migliorare IntelliSense e Già parzialmente fatto. Estendere a tutti gli script per migliorare IntelliSense e
validazione runtime. validazione runtime.
@@ -510,7 +531,7 @@ validazione runtime.
## 6. Scalabilità & estensioni ## 6. Scalabilità & estensioni
### 6.1 [P2] Linux Build VM ### 6.1 [P2] [ ] Linux Build VM
**Documento di pianificazione dedicato**: [docs/LINUX-TEMPLATE-SETUP.md](docs/LINUX-TEMPLATE-SETUP.md) **Documento di pianificazione dedicato**: [docs/LINUX-TEMPLATE-SETUP.md](docs/LINUX-TEMPLATE-SETUP.md)
— bozza completa con fasi A→H, decisioni di design, differenze rispetto al template Windows. — bozza completa con fasi A→H, decisioni di design, differenze rispetto al template Windows.
@@ -524,7 +545,7 @@ Sostituire WinRM con SSH key-based; `Invoke-Command` non funziona — usare `ssh
Aggiungere `scripts/_Transport.psm1` con due implementazioni (`WinRM`, `SSH`) dietro la Aggiungere `scripts/_Transport.psm1` con due implementazioni (`WinRM`, `SSH`) dietro la
stessa interfaccia. `Invoke-CIJob.ps1` sceglie dal parametro `-GuestOS`. stessa interfaccia. `Invoke-CIJob.ps1` sceglie dal parametro `-GuestOS`.
### 6.2 [P3] Workflow generico riutilizzabile (composite action) ### 6.2 [P3] [ ] Workflow generico riutilizzabile (composite action)
File: [gitea/workflow-example.yml](gitea/workflow-example.yml). File: [gitea/workflow-example.yml](gitea/workflow-example.yml).
Trasformare in *composite action* Gitea (`.gitea/actions/local-ci-build/action.yml`) Trasformare in *composite action* Gitea (`.gitea/actions/local-ci-build/action.yml`)
@@ -538,7 +559,7 @@ richiamabile da qualsiasi repo:
``` ```
Riduce la duplicazione su nuovi repo e centralizza l'evoluzione del wrapper. Riduce la duplicazione su nuovi repo e centralizza l'evoluzione del wrapper.
### 6.3 [P3] Multi-host runner federation ### 6.3 [P3] [ ] Multi-host runner federation
Quando 4 VM in parallelo non bastano, l'opzione naturale è un secondo host Windows con Quando 4 VM in parallelo non bastano, l'opzione naturale è un secondo host Windows con
stesso `Setup-Host.ps1` registrato come secondo runner Gitea (label `windows-build:host-2`). stesso `Setup-Host.ps1` registrato come secondo runner Gitea (label `windows-build:host-2`).
Già supportato lato Gitea, basta replicare il setup. Già supportato lato Gitea, basta replicare il setup.
@@ -546,7 +567,7 @@ Già supportato lato Gitea, basta replicare il setup.
**Premessa**: prima di scalare orizzontale, profilare se il collo di bottiglia è CPU **Premessa**: prima di scalare orizzontale, profilare se il collo di bottiglia è CPU
(i9-10900X 20T è abbondante) o I/O (NVMe). Se è I/O, una seconda NVMe sullo stesso host basta. (i9-10900X 20T è abbondante) o I/O (NVMe). Se è I/O, una seconda NVMe sullo stesso host basta.
### 6.4 [P3] Build matrix nel workflow ### 6.4 [P3] [ ] Build matrix nel workflow
File: [gitea/workflows/build-nsis.yml](gitea/workflows/build-nsis.yml). File: [gitea/workflows/build-nsis.yml](gitea/workflows/build-nsis.yml).
Attualmente single job. Quando arriverà la VM Linux: Attualmente single job. Quando arriverà la VM Linux:
@@ -557,11 +578,11 @@ strategy:
runs-on: ${{ matrix.target }}-build runs-on: ${{ matrix.target }}-build
``` ```
### 6.5 [P3] Secret injection workflow-level ### 6.5 [P3] [ ] Secret injection workflow-level
Per chiavi di firma (Authenticode, GPG), usare i Gitea secrets nel workflow e passarli a Per chiavi di firma (Authenticode, GPG), usare i Gitea secrets nel workflow e passarli a
`Invoke-CIJob.ps1` come param + env, mai loggati. Stesso modello di §1.5 per il PAT. `Invoke-CIJob.ps1` come param + env, mai loggati. Stesso modello di §1.5 per il PAT.
### 6.6 [P2] Toolchain Tier-1 in Setup-WinBuild2025 ### 6.6 [P2] [ ] Toolchain Tier-1 in Setup-WinBuild2025
File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1). File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
**Motivazione**: toolchain attuale (`.NET SDK`, `Python`, `VS Build Tools`) copre solo C#/Python/MSBuild. **Motivazione**: toolchain attuale (`.NET SDK`, `Python`, `VS Build Tools`) copre solo C#/Python/MSBuild.
@@ -619,7 +640,7 @@ duplicazioni; ogni concern ha **una sola** sorgente di verità.
**Stato attuale (duplicati)**: UAC, Explorer LaunchTo, Server Manager off, DisableCAD, **Stato attuale (duplicati)**: UAC, Explorer LaunchTo, Server Manager off, DisableCAD,
OOBE telemetry, WU services disable — entrambi gli script li toccano. OOBE telemetry, WU services disable — entrambi gli script li toccano.
### 7.1 [P1] Spostare base OS hardening da Setup a Deploy ### 7.1 [P1] [x] Spostare base OS hardening da Setup a Deploy
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).
@@ -634,7 +655,7 @@ File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
- [x] Setup Step 5c (Server Manager / DisableCAD / OOBE) → Assert-Step only - [x] Setup Step 5c (Server Manager / DisableCAD / OOBE) → Assert-Step only
- [x] E2e validation — completata §7.4 (2026-05-10) - [x] E2e validation — completata §7.4 (2026-05-10)
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle) ### 7.2 [P1] [x] 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).
@@ -649,13 +670,13 @@ avvia WU via SchTask SYSTEM, poi Step 6b disabilita servizi permanentemente post
- [x] Setup Step 6b: già disabilita wuauserv/UsoSvc + GPO keys — nessuna modifica necessaria - [x] Setup Step 6b: già disabilita wuauserv/UsoSvc + GPO keys — nessuna modifica necessaria
- [x] E2e validation — completata §7.4 (2026-05-10) - [x] E2e validation — completata §7.4 (2026-05-10)
### 7.3 [P2] Update header docs di entrambi gli script ### 7.3 [P2] [x] Update header docs di entrambi gli script
- [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) - [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)
- [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 - [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
- [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 - [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
### 7.4 [P2] Test e2e refactor — COMPLETATO 2026-05-10 ### 7.4 [P2] [x] Test e2e refactor — COMPLETATO 2026-05-10
- [x] Deploy stand-alone su VM test → `Get-NetFirewallProfile` tutti `Enabled=False`, `Get-Service wuauserv | Select StartType``Manual`, `NoAutoUpdate=1` GPO presente - [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 dopo Deploy con `-SkipWindowsUpdate` → tutti `Assert-Step` passano `[OK]` senza eseguire Set (log mostra solo validation output)
@@ -668,7 +689,7 @@ avvia WU via SchTask SYSTEM, poi Step 6b disabilita servizi permanentemente post
- Deploy + Setup: autologin Administrator (`AutoAdminLogon=1`) aggiunto in post-install.ps1 e validato in Assert-Step 5c - 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` - 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 —
non setup-a la build VM, **estende** un template OS già pronto con toolchain CI. non setup-a la build VM, **estende** un template OS già pronto con toolchain CI.
Rinominare in `Setup-CITools.ps1` o `Install-CIToolchain.ps1` rifletterebbe meglio la Rinominare in `Setup-CITools.ps1` o `Install-CIToolchain.ps1` rifletterebbe meglio la
+4 -4
View File
@@ -53,7 +53,7 @@ The runner host **never executes build tools directly**. Its only role is orches
│ │ ├── Clone_Job_002.vmx │ │ │ │ ├── Clone_Job_002.vmx │ │
│ │ └── Clone_Job_003.vmx │ │ │ │ └── Clone_Job_003.vmx │ │
│ └───────────────────────────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────────────────────┘ │
│ │ WinRM :5985 (192.168.79.0/24 NAT) │ │ WinRM :5986 HTTPS (192.168.79.0/24 NAT) │
└──────────────────────────────────┼──────────────────────────────────────┘ └──────────────────────────────────┼──────────────────────────────────────┘
┌────────────────────────┼──────────────────────────┐ ┌────────────────────────┼──────────────────────────┐
@@ -105,8 +105,8 @@ The runner host **never executes build tools directly**. Its only role is orches
### WinRM / PowerShell Remoting ### WinRM / PowerShell Remoting
- Default transport for remote build execution inside VMs - Default transport for remote build execution inside VMs
- Port: **5985** (HTTP, in-lab use); migrate to 5986/HTTPS for hardened setups - Port: **5986** (HTTPS, self-signed cert, `AllowUnencrypted=false`)
- Authentication: **Basic** (unencrypted — acceptable for isolated lab) - Authentication: **Basic over HTTPS** (`-SkipCACheck` for lab self-signed cert)
- Used for: - Used for:
- Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest - Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest
- Running build commands (`Invoke-Command`) - Running build commands (`Invoke-Command`)
@@ -133,7 +133,7 @@ Build VMs:
... ...
Clone_Job_N → 192.168.79.1xx Clone_Job_N → 192.168.79.1xx
WinRM port 5985 (HTTP) on each VM IP. WinRM port 5986 (HTTPS, self-signed) on each VM IP.
VMs have internet access via NAT — required for pip/nuget during build. VMs have internet access via NAT — required for pip/nuget during build.
``` ```
+12 -33
View File
@@ -30,47 +30,26 @@ $cred = Get-StoredCredential -Target 'BuildVMGuest'
--- ---
## 2. WinRM Security ## 2. WinRM Security — HTTPS/5986 (implementato 2026-05-10)
### Current setup (HTTP / port 5985) ### Setup attuale (HTTPS / port 5986)
Acceptable for an **isolated lab network** where: `Deploy-WinBuild2025.ps1` post-install.ps1 crea un certificato self-signed e configura
- Build VMs sono su VMnet8 NAT (192.168.79.0/24) — raggiungibili dall'host, internet via NAT il listener HTTPS/5986 **prima** dello snapshot `BaseClean`. `AllowUnencrypted=false`.
- No external traffic reaches port 5985
- Credentials are managed via Windows Credential Manager
### Recommended upgrade: WinRM over HTTPS (port 5986) - Build VMs su VMnet8 NAT (192.168.79.0/24) — accesso solo dall'host
- Port 5986 firewall rule ristretta a `RemoteAddress '192.168.79.0/24'`
- Credentials via Windows Credential Manager (target `BuildVMGuest`)
Tutti gli script host usano:
```powershell ```powershell
# Inside the template VM (before taking BaseClean snapshot):
# Create self-signed certificate
$cert = New-SelfSignedCertificate `
-Subject "CN=$(hostname)" `
-CertStoreLocation Cert:\LocalMachine\My `
-KeyUsage DigitalSignature, KeyEncipherment `
-KeyAlgorithm RSA `
-KeyLength 2048
# Create HTTPS WinRM listener
New-WSManInstance WinRM/Config/Listener `
-SelectorSet @{ Transport = 'HTTPS'; Address = '*' } `
-ValueSet @{ Hostname = $(hostname); CertificateThumbprint = $cert.Thumbprint }
# Allow port 5986
New-NetFirewallRule -Name 'WinRM-HTTPS-CI' -DisplayName 'WinRM HTTPS CI' `
-Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
```
```powershell
# On the HOST (in scripts), use HTTPS session options:
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL ` $session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL -Authentication Basic `
-Credential $cred -SessionOption $sessionOptions -Credential $cred -SessionOption $sessionOptions
``` ```
> `-SkipCACheck` is acceptable for a self-signed cert in an isolated lab. Do NOT > `-SkipCACheck`/`-SkipCNCheck` sono accettabili per un cert self-signed in lab isolato.
> use this against externally accessible machines. > Non usare contro macchine accessibili dall'esterno — usare una CA trusted in quel caso.
--- ---
@@ -251,7 +230,7 @@ Invoke-Command -Session $session -ScriptBlock {
``` ```
Build VMs can reach: Build VMs can reach:
- The host via VMnet8 gateway (WinRM on port 5985) - The host via VMnet8 gateway (WinRM HTTPS on port 5986)
- Internet via VMware NAT (for pip, nuget, npm at build time) - Internet via VMware NAT (for pip, nuget, npm at build time)
- Gitea server if on LAN reachable via NAT gateway - Gitea server if on LAN reachable via NAT gateway
+2 -2
View File
@@ -120,10 +120,10 @@ Allo `Setup-Host.ps1` exit 0:
3. **Configura host WinRM client** (necessario per Prepare-WinBuild2025.ps1): 3. **Configura host WinRM client** (necessario per Prepare-WinBuild2025.ps1):
```powershell ```powershell
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force # AllowUnencrypted non serve — Prepare usa HTTPS/5986 (nessun testo in chiaro)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
``` ```
*Note*: Prepare-WinBuild2025.ps1 imposta queste in modo transitorio e le ripristina nel `finally`. *Note*: Prepare-WinBuild2025.ps1 aggiunge l'IP specifico della VM in modo transitorio e lo ripristina nel `finally`.
4. **Provisiona il template VM** — vedi [WINDOWS-TEMPLATE-SETUP.md](WINDOWS-TEMPLATE-SETUP.md) 4. **Provisiona il template VM** — vedi [WINDOWS-TEMPLATE-SETUP.md](WINDOWS-TEMPLATE-SETUP.md)
+1 -1
View File
@@ -314,7 +314,7 @@ Equivalente di `Prepare-WinBuild2025.ps1` per Linux. Usa SSH invece di WinRM.
| Aspetto | Windows | Linux | | Aspetto | Windows | Linux |
| ----------------------- | -------------------------------------- | ------------------------------------------- | | ----------------------- | -------------------------------------- | ------------------------------------------- |
| Transport | WinRM HTTP/5985 + Basic | SSH/22 + ed25519 key | | Transport | WinRM HTTPS/5986 + Basic (-SkipCACheck) | SSH/22 + ed25519 key |
| Auth fluid | `ci_build` admin + UAC off + LATFP=1 | `ci_build` + sudoers NOPASSWD | | Auth fluid | `ci_build` admin + UAC off + LATFP=1 | `ci_build` + sudoers NOPASSWD |
| Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` | | Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` |
| Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` | | Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` |
+9 -8
View File
@@ -65,7 +65,8 @@ winrm quickconfig -q
```powershell ```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force
Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force # AllowUnencrypted left false — Deploy post-install.ps1 creates HTTPS/5986 listener
# Basic auth is secure over HTTPS; host connects on port 5986 with -SkipCACheck
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
``` ```
@@ -110,12 +111,12 @@ cd N:\Code\Workspace\Local-CI-CD-System\template
Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass): Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass):
1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5985 reachable 1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5986 reachable
2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts` 2. **Configura host WinRM client**: aggiunge `$VMIPAddress` ai `TrustedHosts` (no `AllowUnencrypted` — HTTPS non lo richiede)
- Salva valori precedenti, ripristinati nel `finally` (host posture invariata) - Salva valore precedente, ripristinato nel `finally` (host posture invariata)
3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP 3. **Validazione host WinRM**: `TrustedHosts` include IP
4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin 4. **Test connettività**: `Test-WSMan -Port 5986 -UseSSL -Authentication Basic` con credenziali admin
5. **Apre PSSession** verso la VM 5. **Apre PSSession** `-UseSSL -Port 5986` verso la VM
6. **Crea `C:\CI`** in guest, valida creazione 6. **Crea `C:\CI`** in guest, valida creazione
7. **Copia `Setup-WinBuild2025.ps1`** in `C:\CI\Setup-WinBuild2025.ps1`, valida size match 7. **Copia `Setup-WinBuild2025.ps1`** in `C:\CI\Setup-WinBuild2025.ps1`, valida size match
8. **Esegue `Setup-WinBuild2025.ps1`** dentro la VM (vedi Fase C) 8. **Esegue `Setup-WinBuild2025.ps1`** dentro la VM (vedi Fase C)
@@ -193,7 +194,7 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
| ---- | ---------- | ------------------------------------------------------------------------------ | | ---- | ---------- | ------------------------------------------------------------------------------ |
| 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` | | 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` |
| 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) | | 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) |
| 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 | | 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=false (HTTPS-only), Auth/Basic=true, MaxMem≥2048 |
| 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators | | 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Operativo | Ogni dir Test-Path -PathType Container | | 5 | Operativo | Ogni dir Test-Path -PathType Container |
+8 -5
View File
@@ -39,7 +39,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $IPAddress, [string] $IPAddress,
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -67,10 +67,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..." Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..."
$session = New-PSSession ` $session = New-PSSession `
-ComputerName $IPAddress ` -ComputerName $IPAddress `
-Credential $Credential ` -Credential $Credential `
-SessionOption $sessionOptions ` -SessionOption $sessionOptions `
-ErrorAction Stop -UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try { try {
# ── Verify artifact exists in guest ────────────────────────────────── # ── Verify artifact exists in guest ──────────────────────────────────
+1 -1
View File
@@ -103,7 +103,7 @@ param(
[string] $ArtifactBaseDir = 'F:\CI\Artifacts', [string] $ArtifactBaseDir = 'F:\CI\Artifacts',
[ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $VMIPAddress = '', # if empty, auto-detected via vmrun getGuestIPAddress [string] $VMIPAddress = '', # if empty, auto-detected via vmrun getGuestIPAddress
[string] $GuestCredentialTarget = 'BuildVMGuest', [string] $GuestCredentialTarget = 'BuildVMGuest',
+8 -5
View File
@@ -49,7 +49,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $IPAddress, [string] $IPAddress,
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -85,10 +85,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..." Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..."
$session = New-PSSession ` $session = New-PSSession `
-ComputerName $IPAddress ` -ComputerName $IPAddress `
-Credential $Credential ` -Credential $Credential `
-SessionOption $sessionOptions ` -SessionOption $sessionOptions `
-ErrorAction Stop -UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try { try {
# ── Ensure working directories exist on guest ──────────────────────── # ── Ensure working directories exist on guest ────────────────────────
+11 -8
View File
@@ -40,7 +40,7 @@ param(
[string] $VMPath, [string] $VMPath,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $IPAddress, [string] $IPAddress,
[ValidateRange(30, 600)] [ValidateRange(30, 600)]
@@ -52,7 +52,7 @@ param(
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', [string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
# Skip ICMP ping phase (phase 2). Useful when the guest firewall blocks ICMP # Skip ICMP ping phase (phase 2). Useful when the guest firewall blocks ICMP
# but WinRM (TCP 5985) is open. Phase 1 (vmrun state) and phase 3 (WinRM) # but WinRM (TCP 5986) is open. Phase 1 (vmrun state) and phase 3 (WinRM)
# are still checked. # are still checked.
[switch] $SkipPing [switch] $SkipPing
) )
@@ -107,17 +107,20 @@ while ((Get-Date) -lt $deadline) {
} }
} }
# ── Phase 3: WinRM responsive ──────────────────────────────────────── # ── Phase 3: WinRM port 5986 accepting TCP connections ───────────────
try { # Test-WSMan has no -SessionOption in PS 5.1 and fails cert validation for
Test-WSMan -ComputerName $IPAddress -ErrorAction Stop | Out-Null # self-signed certs. TCP open on 5986 = HTTPS listener up = VM ready.
# Success — VM is ready $winrmUp = Test-NetConnection -ComputerName $IPAddress -Port 5986 `
-InformationLevel Quiet -WarningAction SilentlyContinue `
-ErrorAction SilentlyContinue
if ($winrmUp) {
Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)." Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)."
return return
} }
catch { else {
$phase = 'winrm' $phase = 'winrm'
if ($lastPhase -ne $phase) { if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 3/3: ping OK, waiting for WinRM on port 5985..." Write-Host "[Wait-VMReady] Phase 3/3: ping OK, waiting for WinRM on port 5986 (HTTPS)..."
$lastPhase = $phase $lastPhase = $phase
} }
Start-Sleep -Seconds $PollIntervalSeconds Start-Sleep -Seconds $PollIntervalSeconds
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -43,8 +43,8 @@
them after applying updates (§7.2 Strategy B) them after applying updates (§7.2 Strategy B)
- Windows Firewall all profiles disabled (VMnet8 NAT lab; Setup Step 1 validates) - 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 - RDP enabled (no NLA, lab only) + firewall rules for RDP/ICMP/WinRM-HTTPS
- WinRM HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth, - WinRM HTTPS 5986 only (self-signed cert), Basic auth, AllowUnencrypted=false,
AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25 TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25
- ICMPv4 inbound allowed - ICMPv4 inbound allowed
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive), - Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed; DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
@@ -127,7 +127,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
# ── ISOs ── # ── ISOs ──
[string] $WinISO = 'F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso', [string] $WinISO = 'F:\CI\ISO\en-us_windows_server_2025_updated_april_2026_x64_dvd_225d826d.iso',
[string] $ToolsISO = 'F:\CI\ISO\VMware-tools-windows.iso', [string] $ToolsISO = 'F:\CI\ISO\VMware-tools-windows.iso',
# Cache path for the rebuilt no-prompt Windows ISO. Reused across runs when # Cache path for the rebuilt no-prompt Windows ISO. Reused across runs when
@@ -617,17 +617,17 @@ Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyConti
New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null
L 'RDP + ICMP allowed' L 'RDP + ICMP allowed'
# WinRM HTTP + HTTPS, Basic+Unencrypted, TrustedHosts=* (lab only) # WinRM HTTPS-only/5986, Basic auth over HTTPS, TrustedHosts=* (lab only)
# AllowUnencrypted intentionally left false all WinRM traffic uses HTTPS/5986.
Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null
try { try {
`$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My `$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint `$cert.Thumbprint -Force | Out-Null New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint `$cert.Thumbprint -Force | Out-Null
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 -RemoteAddress '192.168.79.0/24' | Out-Null
L 'WinRM HTTPS listener configured' L 'WinRM HTTPS listener configured (port 5986, restricted to 192.168.79.0/24)'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" } } catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null
@@ -1107,8 +1107,7 @@ Write-Host " Admin user : $AdminUser"
Write-Host " Snapshot : $SnapshotName" Write-Host " Snapshot : $SnapshotName"
Write-Host "" Write-Host ""
Write-Host " RDP : mstsc /v:$guestIP" Write-Host " RDP : mstsc /v:$guestIP"
Write-Host " WinRM HTTP : 5985" Write-Host " WinRM HTTPS : 5986 (self-signed, AllowUnencrypted=false)"
Write-Host " WinRM HTTPS : 5986 (self-signed)"
Write-Host "" Write-Host ""
Write-Host " Next steps (from template\ dir):" Write-Host " Next steps (from template\ dir):"
Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP" Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP"
+668
View File
@@ -0,0 +1,668 @@
#Requires -Version 5.1
<#
.SYNOPSIS
HOST-side script: delivers and runs Setup-WinBuild2022.ps1 inside the template VM.
.DESCRIPTION
Automates the template VM provisioning from the host machine:
1. Pre-flight validation: script presence, IP octet range, TCP/5986 reachable
2. Configures host WinRM client (TrustedHosts only, HTTPS needs no AllowUnencrypted) — restored on exit
3. Validates host WinRM TrustedHosts applied correctly
4. Opens PSSession -UseSSL -Port 5986 -SkipCACheck — fails fast with actionable message
(PS 5.1 Test-WSMan has no -SessionOption, so PSSession is used as the connectivity probe)
5. PSSession open — step 4 and 5 are now merged
6. Ensures C:\CI exists on guest; validates creation
7. Copies Setup-WinBuild2022.ps1 into the VM; validates file present + size match
8. Runs Setup-WinBuild2022.ps1 inside the VM with the given parameters
(Setup performs per-step validation internally — aborts on any failure)
9. Handles the reboot-required case (exit 3010) and optionally re-runs
10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command
(WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs)
11. Restores host TrustedHosts in finally block
Every validation step uses Assert-Step: prints [OK] on success, throws on failure.
The snapshot should only be taken after this script exits 0.
PREREQUISITES (do these manually before running this script):
a. VM NIC is set to VMnet8 (NAT) — the VM needs internet for downloads
b. Windows Server is installed and booted
c. WinRM is enabled inside the VM. From an elevated cmd/PS in the VM run:
winrm quickconfig -q
Enable-PSRemoting -Force -SkipNetworkProfileCheck
d. You know the IP address the VM got from DHCP on VMnet8 (NAT)
(check via: ipconfig inside the VM, or VMware → VM → Settings →
Network Adapter → Advanced → MAC address, then check DHCP leases)
AFTER THIS SCRIPT COMPLETES (exit 0, all validations passed):
1. Shut down the VM
2. Take snapshot: VM → Snapshot → Take Snapshot → name: BaseClean
(VM stays on VMnet8 NAT — internet access is required for builds)
3. Store credentials on host (use the same password chosen during provisioning):
New-StoredCredential -Target 'BuildVMGuest' -UserName 'ci_build' `
-Password '<your-build-password>' -Persist LocalMachine
.PARAMETER VMXPath
Full path to the template VM's .vmx file (e.g. F:\CI\Templates\WinBuild\WinBuild.vmx).
When provided:
- If the VM is powered off, the script starts it automatically via vmrun.
- The guest IP is detected automatically via vmrun getGuestIPAddress (requires
VMware Tools installed in the guest).
- At the end, the snapshot is created automatically after provisioning.
Either -VMXPath or -VMIPAddress (or both) must be supplied.
.PARAMETER VMIPAddress
IP address of the template VM on VMnet8 NAT.
Optional when -VMXPath is provided (IP is auto-detected via VMware Tools).
Required when -VMXPath is omitted.
Example: 192.168.79.130
.PARAMETER AdminUsername
Administrator username inside the VM. Default: Administrator
.PARAMETER AdminPassword
Administrator password inside the VM. If not supplied, prompts interactively.
.PARAMETER BuildPassword
Password to set for the ci_build user inside the VM.
Must not be empty — script prompts interactively if not supplied.
.PARAMETER BuildUsername
Local username for the CI build account inside the VM. Default: ci_build.
Passed through to Setup-WinBuild2022.ps1 and used in post-setup validation.
.PARAMETER DotNetSdkVersion
.NET SDK channel to install in the VM. Default: 10.0 (matches host SDK).
.PARAMETER SkipWindowsUpdate
Pass through to Setup-WinBuild2022.ps1 to skip the Windows Update step.
.PARAMETER SnapshotName
Name of the snapshot to create. Default: BaseClean (case-sensitive — used by New-BuildVM.ps1).
Only relevant when -TakeSnapshot is set.
.PARAMETER StoreCredential
After provisioning completes, store BuildUsername/BuildPassword in Windows
Credential Manager (target: CredentialTarget). Installs the CredentialManager
module (CurrentUser scope) automatically if not present.
Equivalent to running manually:
New-StoredCredential -Target 'BuildVMGuest' -UserName 'ci_build' `
-Password '<pwd>' -Persist LocalMachine
.PARAMETER CredentialTarget
Windows Credential Manager target name used when -StoreCredential is set.
Default: BuildVMGuest (matches the target expected by Invoke-CIJob.ps1 and
the other CI scripts that load guest credentials via Get-StoredCredential).
.EXAMPLE
# Fully automated — VMX path only (power-on + IP detection + snapshot automatic):
.\Prepare-WinBuild2022.ps1 `
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' `
-BuildPassword 'StrongCIPass!2026' -StoreCredential
# Skip Windows Update (already applied):
.\Prepare-WinBuild2022.ps1 `
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' `
-SkipWindowsUpdate -StoreCredential
# Explicit IP (manual start, no VMware Tools required for IP detection):
.\Prepare-WinBuild2022.ps1 -VMIPAddress 192.168.79.130 `
-BuildPassword 'StrongCIPass!2026' -StoreCredential
# Both supplied — uses VMXPath for power-on/snapshot, explicit IP skips getGuestIPAddress:
.\Prepare-WinBuild2022.ps1 `
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' -VMIPAddress 192.168.79.130 `
-BuildPassword 'StrongCIPass!2026' -StoreCredential
#>
[CmdletBinding()]
param(
# Path to the VM .vmx — enables auto power-on + IP detection + auto snapshot.
# Either -VMXPath or -VMIPAddress (or both) must be supplied.
[string] $VMXPath = '',
# Guest IP on VMnet8 NAT. Optional when -VMXPath provided (auto-detected via VMware Tools).
[string] $VMIPAddress = '',
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword = '',
[string] $BuildPassword = '',
[string] $BuildUsername = 'ci_build',
[string] $DotNetSdkVersion = '10.0',
[switch] $SkipWindowsUpdate,
# Skip disk cleanup inside the VM (cleanmgr + DISM + cache clear) — speeds up re-runs
[switch] $SkipCleanup,
# Snapshot name (case-sensitive — must match GITEA_CI_SNAPSHOT_NAME in runner/config.yaml)
[string] $SnapshotName = 'BaseClean',
# Store BuildUsername/BuildPassword in Windows Credential Manager after provisioning
[switch] $StoreCredential,
# Credential Manager target name (must match what CI scripts use via Get-StoredCredential)
[string] $CredentialTarget = 'BuildVMGuest'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Locate vmrun.exe ──────────────────────────────────────────────────────────
$vmrunCandidates = @(
'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
'C:\Program Files\VMware\VMware Workstation\vmrun.exe'
)
$vmrunExe = $vmrunCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $vmrunExe) {
$vmrunFound = Get-Command vmrun -ErrorAction SilentlyContinue
if ($vmrunFound) { $vmrunExe = $vmrunFound.Source }
}
if (-not $vmrunExe) { throw "[Prepare] vmrun.exe not found. Install VMware Workstation or add it to PATH." }
# ── Validate input: need VMXPath or VMIPAddress ───────────────────────────────
if ($VMXPath -eq '' -and $VMIPAddress -eq '') {
throw "[Prepare] Provide -VMXPath (auto power-on + IP detection) or -VMIPAddress (or both)."
}
# ── Power on VM and detect IP via VMXPath ────────────────────────────────────
$vmWasPoweredOn = $false
if ($VMXPath -ne '') {
if (-not (Test-Path $VMXPath)) { throw "[Prepare] VMX not found: '$VMXPath'" }
# Check if already running
$runningList = @(& $vmrunExe list 2>&1 |
Where-Object { $_ -match '\.vmx$' } | ForEach-Object { $_.Trim() })
if ($runningList -notcontains $VMXPath) {
Write-Host "[Prepare] VM not running — starting: $VMXPath"
& $vmrunExe start $VMXPath
if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun start exited $LASTEXITCODE." }
$vmWasPoweredOn = $true
Write-Host "[Prepare] VM started. Waiting for guest OS to initialize..."
}
else {
Write-Host "[Prepare] VM already running: $VMXPath"
}
# Auto-detect IP if not supplied
if ($VMIPAddress -eq '') {
Write-Host "[Prepare] Detecting guest IP via VMware Tools (getGuestIPAddress -wait)..."
$detectedIP = (& $vmrunExe getGuestIPAddress $VMXPath -wait 2>&1).Trim()
if ($detectedIP -notmatch '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') {
throw "[Prepare] Could not get guest IP. Got: '$detectedIP'. Ensure VMware Tools is installed."
}
$VMIPAddress = $detectedIP
Write-Host "[Prepare] Guest IP: $VMIPAddress" -ForegroundColor Green
}
}
function Assert-Step {
param(
[Parameter(Mandatory)] [string] $StepName,
[Parameter(Mandatory)] [string] $Description,
[Parameter(Mandatory)] [scriptblock] $Test
)
$ok = $false
try { $ok = [bool](& $Test) }
catch { throw "[Prepare/$StepName] Validation threw on '$Description': $_" }
if (-not $ok) {
throw "[Prepare/$StepName] Validation failed: $Description"
}
Write-Host " [OK] $Description" -ForegroundColor Green
}
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Pre-flight validation
Assert-Step 'PreFlight' 'script directory resolved' { Test-Path $scriptDir -PathType Container }
Assert-Step 'PreFlight' 'Setup-WinBuild2022.ps1 present alongside this script' {
Test-Path (Join-Path $scriptDir 'Setup-WinBuild2022.ps1') -PathType Leaf
}
Assert-Step 'PreFlight' "VMIPAddress octets in 0..255" {
$parts = $VMIPAddress.Split('.') | ForEach-Object { [int]$_ }
($parts.Count -eq 4) -and -not ($parts | Where-Object { $_ -lt 0 -or $_ -gt 255 })
}
# WinRM readiness — retry loop (longer timeout if VM was just powered on by this script)
$winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 }
Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5986 (timeout ${winrmTimeoutSec}s)..."
$winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec)
$winrmReady = $false
while ([datetime]::UtcNow -lt $winrmDeadline) {
if (Test-NetConnection -ComputerName $VMIPAddress -Port 5986 `
-InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) {
$winrmReady = $true; break
}
Start-Sleep -Seconds 5
Write-Host " ... waiting for WinRM..."
}
Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5986" { $winrmReady }
# Prompt for BuildPassword if not supplied — never use a hardcoded default.
if ($BuildPassword -eq '') {
Write-Host "[Prepare] No -BuildPassword supplied. Enter password for the CI build account inside the VM:"
$secPwd = Read-Host -Prompt " BuildPassword" -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)
$BuildPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
# ── Build PSCredential ────────────────────────────────────────────────────────
if ($AdminPassword -eq '') {
Write-Host "[Prepare] Enter administrator credentials for the template VM ($VMIPAddress):"
$credential = Get-Credential -UserName $AdminUsername -Message "Template VM administrator ($VMIPAddress)"
}
else {
$secPwd = ConvertTo-SecureString $AdminPassword -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($AdminUsername, $secPwd)
}
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
# ── Configure HOST-side WinRM client (TrustedHosts for HTTPS Basic auth) ──────
# HTTPS/5986 does not require AllowUnencrypted on the host side. Only TrustedHosts
# needs appending so PS accepts the non-domain VM. Restored in the finally block.
$prevTrustedHosts = $null
try {
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
# Add VM IP to TrustedHosts if not already present
if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") {
$newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop
}
Write-Host "[Prepare] Host TrustedHosts configured."
Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" {
$th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
$th -eq '*' -or $th -like "*$VMIPAddress*"
}
}
catch {
Write-Warning "[Prepare] Could not configure host WinRM TrustedHosts (need elevation?)."
Write-Warning "Run once in an elevated PowerShell on the HOST:"
Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force"
Write-Warning "Then re-run this script."
exit 1
}
# ── Open session (Test-WSMan skipped — PS 5.1 Test-WSMan has no -SessionOption for HTTPS) ──
# New-PSSession with -SkipCACheck handles cert validation for self-signed lab cert.
Write-Host "[Prepare] Opening PSSession to $VMIPAddress`:5986 (HTTPS)..."
$session = try {
New-PSSession `
-ComputerName $VMIPAddress `
-Credential $credential `
-SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
} catch {
Write-Error @"
[Prepare] Cannot open PSSession to $VMIPAddress via WinRM HTTPS (port 5986).
Ensure:
- The VM is running and on VMnet8 (NAT) with internet access
- WinRM HTTPS is enabled inside the VM Deploy-WinBuild2022.ps1 post-install.ps1
must have run successfully (creates self-signed cert + HTTPS listener on 5986)
- Windows Firewall allows port 5986 inbound (or firewall is disabled on the guest)
- The IP is correct (check ipconfig inside the VM)
Error: $_
"@
exit 1
}
try {
# ── Copy Setup-WinBuild2022.ps1 into the VM ───────────────────────────────
$setupScript = Join-Path $scriptDir 'Setup-WinBuild2022.ps1'
$guestScriptPath = 'C:\CI\Setup-WinBuild2022.ps1'
Write-Host "[Prepare] Ensuring C:\CI exists on guest..."
Invoke-Command -Session $session -ScriptBlock {
if (-not (Test-Path 'C:\CI')) {
New-Item -ItemType Directory -Path 'C:\CI' -Force | Out-Null
}
}
Assert-Step 'GuestPrep' 'C:\CI exists on guest' {
Invoke-Command -Session $session -ScriptBlock { Test-Path 'C:\CI' -PathType Container }
}
Write-Host "[Prepare] Copying Setup-WinBuild2022.ps1 -> guest $guestScriptPath (UTF-8 BOM)..."
# Do NOT use Copy-Item -ToSession: it binary-copies the file.
# If the source file has no UTF-8 BOM, PowerShell 5.1 on the guest reads it as
# Windows-1252, misinterpreting multi-byte UTF-8 sequences (e.g. em dash U+2014
# encodes as E2 80 94; byte 0x94 in Win-1252 = curly right-quote, which PS 5.1
# treats as a string terminator → parse errors throughout the script).
#
# Fix: read content on host as Unicode string, transmit via WinRM (XML/Unicode),
# write on guest with explicit UTF-8 BOM via System.Text.UTF8Encoding($true).
# PS 5.1 detects the BOM and reads the file as UTF-8 correctly.
$scriptContent = [System.IO.File]::ReadAllText($setupScript, [System.Text.Encoding]::UTF8)
Invoke-Command -Session $session -ScriptBlock {
param($content, $path)
$utf8Bom = New-Object System.Text.UTF8Encoding($true) # $true = emit BOM
[System.IO.File]::WriteAllText($path, $content, $utf8Bom)
} -ArgumentList $scriptContent, $guestScriptPath
# Validation — file present and large enough (BOM transfer changes byte count vs host)
$hostSize = (Get-Item $setupScript).Length
Assert-Step 'GuestPrep' "guest script present at $guestScriptPath" {
Invoke-Command -Session $session -ScriptBlock { param($p) Test-Path $p -PathType Leaf } -ArgumentList $guestScriptPath
}
Assert-Step 'GuestPrep' "guest script size reasonable (host=$hostSize bytes ±6 for BOM)" {
$remoteSize = Invoke-Command -Session $session -ScriptBlock {
param($p) (Get-Item $p -ErrorAction SilentlyContinue).Length
} -ArgumentList $guestScriptPath
# UTF-8 BOM adds 3 bytes; allow small variance
[int64]$remoteSize -ge ([int64]$hostSize - 6) -and [int64]$remoteSize -le ([int64]$hostSize + 6)
}
# ── Build argument list for Setup-WinBuild2022.ps1 ───────────────────────
$setupArgs = @{
BuildPassword = $BuildPassword
BuildUsername = $BuildUsername
DotNetSdkVersion = $DotNetSdkVersion
AdminPassword = $AdminPassword
}
if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true }
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
# ── Run Setup-WinBuild2022.ps1 inside the VM ──────────────────────────────
# Setup may return exit 3010 (ERROR_SUCCESS_REBOOT_REQUIRED) when Windows
# Update applied patches that need a reboot. We loop: reboot guest, wait for
# WinRM, reopen session, re-invoke Setup. Steps 1-5 are idempotent (~30 s);
# Step 6 picks up where the previous run left off (no leftover updates →
# exit 0). Hard cap on iterations to avoid an infinite loop.
$maxWuIterations = 10
$rebootDeadlineMin = 20
function Invoke-GuestSetup {
param([Parameter(Mandatory)] $Session,
[Parameter(Mandatory)] [string] $ScriptPath,
[Parameter(Mandatory)] [hashtable] $ScriptArgs)
Invoke-Command -Session $Session -ScriptBlock {
param($scriptPath, $scriptArgs)
Set-ExecutionPolicy Bypass -Scope Process -Force
$exitCode = 0
try {
& $scriptPath @scriptArgs
$exitCode = $LASTEXITCODE
if ($null -eq $exitCode) { $exitCode = 0 }
} catch {
Write-Error $_
$exitCode = 1
}
return $exitCode
} -ArgumentList $ScriptPath, $ScriptArgs
}
function Wait-GuestWinRMReady {
param([Parameter(Mandatory)] [string] $IP,
[Parameter(Mandatory)] $Cred,
[Parameter(Mandatory)] $SessionOptions,
[int] $TimeoutMin = 20)
$deadline = (Get-Date).AddMinutes($TimeoutMin)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 10
try {
# Test-WSMan has no -SessionOption in PS 5.1 — use New-PSSession as probe.
$probe = New-PSSession -ComputerName $IP -Port 5986 -UseSSL `
-SessionOption $SessionOptions -Credential $Cred `
-Authentication Basic -ErrorAction Stop
Remove-PSSession $probe -ErrorAction SilentlyContinue
return $true
} catch { }
}
return $false
}
Write-Host "[Prepare] Running Setup-WinBuild2022.ps1 inside VM (this will take a while)..."
for ($iter = 1; $iter -le $maxWuIterations; $iter++) {
Write-Host "[Prepare] Setup iteration $iter/$maxWuIterations"
Write-Host "[Prepare] Output from guest:"
Write-Host "------------------------------------------------------------"
$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 "[Prepare] Iteration $iter exit code: $result"
if ($result -eq 0) { break }
if ($result -ne 3010) {
throw "Setup-WinBuild2022.ps1 exited with code $result"
}
# exit 3010 (or transport error treated as 3010) → WU needs reboot.
# Reboot guest if session still alive, wait for WinRM, reopen session, loop.
if ($iter -eq $maxWuIterations) {
throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)"
}
Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..."
if ($session) {
try {
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue
} catch { }
Remove-PSSession $session -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 30 # let WinRM tear down before polling
Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..."
if (-not (Wait-GuestWinRMReady -IP $VMIPAddress -Cred $credential `
-SessionOptions $sessionOptions -TimeoutMin $rebootDeadlineMin)) {
throw "Guest did not come back online within $rebootDeadlineMin min after WU reboot"
}
Write-Host "[Prepare] Guest WinRM ready, reopening session..."
$session = New-PSSession -ComputerName $VMIPAddress -Credential $credential `
-SessionOption $sessionOptions -UseSSL -Port 5986 `
-Authentication Basic -ErrorAction Stop
}
# ── Post-setup remote validation ──────────────────────────────────────────
Write-Host "[Prepare] Running post-setup validation against guest..."
$guestState = Invoke-Command -Session $session -ScriptBlock {
param($BuildUser)
$msbuildPath = 'C:\Program Files (x86)\Microsoft Visual Studio\2026\BuildTools\MSBuild\Current\Bin\MSBuild.exe'
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH','Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('PATH','User')
[PSCustomObject]@{
WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running'
UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue)
UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser)
UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0
FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true)
DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) }
PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf
MSBuildExists = Test-Path $msbuildPath -PathType Leaf
CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts')
}
} -ArgumentList $BuildUsername
Assert-Step 'PostSetup' 'guest WinRM Running' { $guestState.WinRMRunning }
Assert-Step 'PostSetup' "guest user '$BuildUsername' exists" { $guestState.UserExists }
Assert-Step 'PostSetup' "guest user '$BuildUsername' is admin" { $guestState.UserIsAdmin }
Assert-Step 'PostSetup' 'guest UAC disabled' { $guestState.UACDisabled }
Assert-Step 'PostSetup' 'guest firewall disabled' { $guestState.FirewallOff }
Assert-Step 'PostSetup' 'guest .NET SDK installed' { [bool]$guestState.DotNetVersion }
Assert-Step 'PostSetup' 'guest Python installed' { $guestState.PythonExists }
Assert-Step 'PostSetup' 'guest MSBuild present' { $guestState.MSBuildExists }
Assert-Step 'PostSetup' 'guest C:\CI\{build,output,scripts} present' { $guestState.CIDirsPresent }
Write-Host "[Prepare] All post-setup validations passed (.NET $($guestState.DotNetVersion))." -ForegroundColor Green
# ── Store credentials in Windows Credential Manager (optional) ────────────
if ($StoreCredential) {
Write-Host "[Prepare] Storing CI credentials in Windows Credential Manager (target: '$CredentialTarget')..."
if (-not (Get-Module -ListAvailable -Name CredentialManager)) {
Write-Host "[Prepare] CredentialManager module not found — installing (CurrentUser scope)..."
Install-Module -Name CredentialManager -Scope CurrentUser -Force -ErrorAction Stop
}
Import-Module CredentialManager -ErrorAction Stop
New-StoredCredential `
-Target $CredentialTarget `
-UserName $BuildUsername `
-Password $BuildPassword `
-Persist LocalMachine `
-ErrorAction Stop | Out-Null
Write-Host "[Prepare] Credentials stored: target='$CredentialTarget' user='$BuildUsername'." -ForegroundColor Green
}
Write-Host "[Prepare] All validations passed." -ForegroundColor Green
# ── Shutdown + snapshot prompt ─────────────────────────────────────────────
# vmrun.exe already located at script start ($vmrunExe)
# Auto-discover VMX path via 'vmrun list' while VM is still running (if not known)
if ($VMXPath -eq '') {
Write-Host "[Prepare] Discovering VMX path from running VMs..."
$runningVMs = @(& $vmrunExe list 2>&1 |
Where-Object { $_ -match '\.vmx$' } |
ForEach-Object { $_.Trim() })
if ($runningVMs.Count -eq 0) {
throw "[Prepare] No running VMs found via 'vmrun list'. Is the template VM still up?"
}
elseif ($runningVMs.Count -eq 1) {
$VMXPath = $runningVMs[0]
Write-Host "[Prepare] VMX auto-detected: $VMXPath" -ForegroundColor Green
}
else {
# Multiple VMs — prefer one under \CI\Templates\
$ciVMs = @($runningVMs | Where-Object { $_ -like '*\CI\Templates\*' })
if ($ciVMs.Count -eq 1) {
$VMXPath = $ciVMs[0]
Write-Host "[Prepare] VMX auto-detected (CI template): $VMXPath" -ForegroundColor Green
}
else {
Write-Host "[Prepare] Multiple VMs running — select one:"
for ($i = 0; $i -lt $runningVMs.Count; $i++) {
Write-Host " [$($i+1)] $($runningVMs[$i])"
}
$idx = [int](Read-Host "[Prepare] Enter number") - 1
if ($idx -lt 0 -or $idx -ge $runningVMs.Count) { throw "[Prepare] Invalid selection." }
$VMXPath = $runningVMs[$idx]
}
}
}
if (-not (Test-Path $VMXPath)) { throw "[Prepare] VMX not found on disk: '$VMXPath'" }
# Prompt
Write-Host "------------------------------------------------------------"
Write-Host ""
$choice = Read-Host "[Prepare] Shut down VM and create snapshot '$SnapshotName'? [Y/N]"
if ($choice -match '^[Yy]') {
# Send graceful shutdown
Write-Host "[Prepare] Sending graceful shutdown to VM..."
try {
Invoke-Command -Session $session -ScriptBlock { Stop-Computer -Force } -ErrorAction Stop
Write-Host "[Prepare] Shutdown command sent." -ForegroundColor Green
}
catch {
Write-Warning "[Prepare] Shutdown command failed (VM may have already shut down): $_"
}
# Close session — VM is shutting down, no further WinRM needed
Remove-PSSession $session -ErrorAction SilentlyContinue
# Wait for VM to power off (poll vmrun list)
Write-Host "[Prepare] Waiting for VM to power off (timeout 120 s)..."
$shutdownDeadline = [datetime]::UtcNow.AddSeconds(120)
$poweredOff = $false
while ([datetime]::UtcNow -lt $shutdownDeadline) {
$runningList = & $vmrunExe list 2>&1 | Out-String
if ($runningList -notmatch [regex]::Escape($VMXPath)) { $poweredOff = $true; break }
Start-Sleep -Seconds 5
Write-Host " ... waiting for shutdown..."
}
if (-not $poweredOff) { throw "[Prepare] VM did not power off within 120 s. Check VMware Workstation." }
# Check for existing snapshot with same name
$existingSnaps = & $vmrunExe listSnapshots $VMXPath 2>&1 | Out-String
$doCreate = $true
if ($existingSnaps -match [regex]::Escape($SnapshotName)) {
Write-Warning "[Prepare] Snapshot '$SnapshotName' already exists."
$delChoice = Read-Host "[Prepare] Delete existing snapshot and recreate? [Y/N]"
if ($delChoice -match '^[Yy]') {
Write-Host "[Prepare] Deleting existing snapshot '$SnapshotName'..."
& $vmrunExe deleteSnapshot $VMXPath $SnapshotName
if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun deleteSnapshot exited $LASTEXITCODE." }
Write-Host "[Prepare] Existing snapshot deleted." -ForegroundColor Green
}
else {
Write-Host "[Prepare] Keeping existing snapshot — skipping creation." -ForegroundColor Yellow
$doCreate = $false
}
}
# Create snapshot
if ($doCreate) {
Write-Host "[Prepare] Creating snapshot '$SnapshotName'..." -ForegroundColor Green
& $vmrunExe -T ws snapshot $VMXPath $SnapshotName
if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun snapshot exited $LASTEXITCODE." }
Write-Host "[Prepare] Snapshot '$SnapshotName' created successfully." -ForegroundColor Green
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Template VM provisioning complete!" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
if ($StoreCredential) {
Write-Host " [DONE] Credentials stored in Credential Manager (target: '$CredentialTarget')." -ForegroundColor Green
}
Write-Host ""
}
else {
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Template VM provisioning complete!" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host ""
Write-Host " MANUAL STEPS REQUIRED:"
Write-Host ""
Write-Host " 1. Shut down the VM: Start -> Shut down"
Write-Host " (VM stays on VMnet8 NAT for internet access during builds)"
Write-Host ""
Write-Host " 2. Take snapshot in VMware Workstation:"
Write-Host " VM -> Snapshot -> Take Snapshot"
Write-Host " Name it EXACTLY: $SnapshotName"
Write-Host ""
if ($StoreCredential) {
Write-Host " 3. [DONE] Credentials stored in Credential Manager (target: '$CredentialTarget')." -ForegroundColor Green
}
else {
Write-Host " 3. Store CI credentials on this HOST (run in elevated PowerShell):"
Write-Host " .\Prepare-WinBuild2022.ps1 -VMIPAddress $VMIPAddress -StoreCredential"
Write-Host " (or manually:)"
Write-Host " New-StoredCredential -Target '$CredentialTarget' ``"
Write-Host " -UserName '$BuildUsername' -Password '<your-build-password>' ``"
Write-Host " -Persist LocalMachine"
}
Write-Host ""
}
}
finally {
Remove-PSSession $session -ErrorAction SilentlyContinue
# Restore host TrustedHosts to its original value
if ($null -ne $prevTrustedHosts) {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue
}
Write-Host "[Prepare] Host TrustedHosts restored."
}
+44 -57
View File
@@ -5,11 +5,12 @@
.DESCRIPTION .DESCRIPTION
Automates the template VM provisioning from the host machine: Automates the template VM provisioning from the host machine:
1. Pre-flight validation: script presence, IP octet range, TCP/5985 reachable 1. Pre-flight validation: script presence, IP octet range, TCP/5986 reachable
2. Configures host WinRM client (AllowUnencrypted, TrustedHosts) — restored on exit 2. Configures host WinRM client (TrustedHosts only, HTTPS needs no AllowUnencrypted) — restored on exit
3. Validates host WinRM client settings applied correctly 3. Validates host WinRM TrustedHosts applied correctly
4. Tests WinRM connectivity (Test-WSMan) — fails fast with actionable message 4. Opens PSSession -UseSSL -Port 5986 -SkipCACheck — fails fast with actionable message
5. Opens PSSession to the VM (PS 5.1 Test-WSMan has no -SessionOption, so PSSession is used as the connectivity probe)
5. PSSession open — step 4 and 5 are now merged
6. Ensures C:\CI exists on guest; validates creation 6. Ensures C:\CI exists on guest; validates creation
7. Copies Setup-WinBuild2025.ps1 into the VM; validates file present + size match 7. Copies Setup-WinBuild2025.ps1 into the VM; validates file present + size match
8. Runs Setup-WinBuild2025.ps1 inside the VM with the given parameters 8. Runs Setup-WinBuild2025.ps1 inside the VM with the given parameters
@@ -17,7 +18,7 @@
9. Handles the reboot-required case (exit 3010) and optionally re-runs 9. Handles the reboot-required case (exit 3010) and optionally re-runs
10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command 10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command
(WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs) (WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs)
11. Restores host WinRM client settings in finally block 11. Restores host TrustedHosts in finally block
Every validation step uses Assert-Step: prints [OK] on success, throws on failure. Every validation step uses Assert-Step: prints [OK] on success, throws on failure.
The snapshot should only be taken after this script exits 0. The snapshot should only be taken after this script exits 0.
@@ -227,18 +228,18 @@ Assert-Step 'PreFlight' "VMIPAddress octets in 0..255" {
# WinRM readiness — retry loop (longer timeout if VM was just powered on by this script) # WinRM readiness — retry loop (longer timeout if VM was just powered on by this script)
$winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 } $winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 }
Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5985 (timeout ${winrmTimeoutSec}s)..." Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5986 (timeout ${winrmTimeoutSec}s)..."
$winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec) $winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec)
$winrmReady = $false $winrmReady = $false
while ([datetime]::UtcNow -lt $winrmDeadline) { while ([datetime]::UtcNow -lt $winrmDeadline) {
if (Test-NetConnection -ComputerName $VMIPAddress -Port 5985 ` if (Test-NetConnection -ComputerName $VMIPAddress -Port 5986 `
-InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) { -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) {
$winrmReady = $true; break $winrmReady = $true; break
} }
Start-Sleep -Seconds 5 Start-Sleep -Seconds 5
Write-Host " ... waiting for WinRM..." Write-Host " ... waiting for WinRM..."
} }
Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5985" { $winrmReady } Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5986" { $winrmReady }
# Prompt for BuildPassword if not supplied — never use a hardcoded default. # Prompt for BuildPassword if not supplied — never use a hardcoded default.
if ($BuildPassword -eq '') { if ($BuildPassword -eq '') {
@@ -261,59 +262,53 @@ else {
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
# ── Configure HOST-side WinRM client (required for Basic auth over HTTP) ────── # ── Configure HOST-side WinRM client (TrustedHosts for HTTPS Basic auth) ──────
# AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value # HTTPS/5986 does not require AllowUnencrypted on the host side. Only TrustedHosts
# and restore it in the finally block so this script doesn't permanently change # needs appending so PS accepts the non-domain VM. Restored in the finally block.
# the host security posture. $prevTrustedHosts = $null
Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..."
$prevAllowUnencrypted = $null
$prevTrustedHosts = $null
try { try {
$prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop
# Add VM IP to TrustedHosts if not already present # Add VM IP to TrustedHosts if not already present
if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") { if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") {
$newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" } $newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop
} }
Write-Host "[Prepare] Host WinRM client configured." Write-Host "[Prepare] Host TrustedHosts configured."
# Validation
Assert-Step 'HostWinRM' 'Client/AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value -eq 'true'
}
Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" { Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" {
$th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
$th -eq '*' -or $th -like "*$VMIPAddress*" $th -eq '*' -or $th -like "*$VMIPAddress*"
} }
} }
catch { catch {
Write-Warning "[Prepare] Could not configure host WinRM client settings (need elevation?)." Write-Warning "[Prepare] Could not configure host WinRM TrustedHosts (need elevation?)."
Write-Warning "Run once in an elevated PowerShell on the HOST:" Write-Warning "Run once in an elevated PowerShell on the HOST:"
Write-Warning " Set-Item WSMan:\localhost\Client\AllowUnencrypted `$true -Force"
Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force" Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force"
Write-Warning "Then re-run this script." Write-Warning "Then re-run this script."
exit 1 exit 1
} }
Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..." # ── Open session (Test-WSMan skipped — PS 5.1 Test-WSMan has no -SessionOption for HTTPS) ──
try { # New-PSSession with -SkipCACheck handles cert validation for self-signed lab cert.
Test-WSMan -ComputerName $VMIPAddress -Credential $credential ` Write-Host "[Prepare] Opening PSSession to $VMIPAddress`:5986 (HTTPS)..."
-Authentication Basic -ErrorAction Stop | Out-Null $session = try {
Write-Host "[Prepare] WinRM reachable." New-PSSession `
} -ComputerName $VMIPAddress `
catch { -Credential $credential `
-SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
} catch {
Write-Error @" Write-Error @"
[Prepare] Cannot reach $VMIPAddress via WinRM. [Prepare] Cannot open PSSession to $VMIPAddress via WinRM HTTPS (port 5986).
Ensure: Ensure:
- The VM is running and on VMnet8 (NAT) with internet access - The VM is running and on VMnet8 (NAT) with internet access
- WinRM is enabled inside the VM (run as Administrator in VM): - WinRM HTTPS is enabled inside the VM Deploy-WinBuild2025.ps1 post-install.ps1
winrm quickconfig -q must have run successfully (creates self-signed cert + HTTPS listener on 5986)
Enable-PSRemoting -Force -SkipNetworkProfileCheck - Windows Firewall allows port 5986 inbound (or firewall is disabled on the guest)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
- Windows Firewall allows port 5985 inbound
- The IP is correct (check ipconfig inside the VM) - The IP is correct (check ipconfig inside the VM)
Error: $_ Error: $_
@@ -321,15 +316,6 @@ Error: $_
exit 1 exit 1
} }
# ── Open session ──────────────────────────────────────────────────────────────
Write-Host "[Prepare] Opening PSSession..."
$session = New-PSSession `
-ComputerName $VMIPAddress `
-Credential $credential `
-SessionOption $sessionOptions `
-Authentication Basic `
-ErrorAction Stop
try { try {
# ── Copy Setup-WinBuild2025.ps1 into the VM ─────────────────────────────── # ── Copy Setup-WinBuild2025.ps1 into the VM ───────────────────────────────
$setupScript = Join-Path $scriptDir 'Setup-WinBuild2025.ps1' $setupScript = Join-Path $scriptDir 'Setup-WinBuild2025.ps1'
@@ -423,9 +409,12 @@ try {
while ((Get-Date) -lt $deadline) { while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
try { try {
$r = Test-WSMan -ComputerName $IP -Credential $Cred ` # Test-WSMan has no -SessionOption in PS 5.1 — use New-PSSession as probe.
-Authentication Basic -ErrorAction Stop $probe = New-PSSession -ComputerName $IP -Port 5986 -UseSSL `
if ($r) { return $true } -SessionOption $SessionOptions -Credential $Cred `
-Authentication Basic -ErrorAction Stop
Remove-PSSession $probe -ErrorAction SilentlyContinue
return $true
} catch { } } catch { }
} }
return $false return $false
@@ -481,7 +470,8 @@ try {
} }
Write-Host "[Prepare] Guest WinRM ready, reopening session..." Write-Host "[Prepare] Guest WinRM ready, reopening session..."
$session = New-PSSession -ComputerName $VMIPAddress -Credential $credential ` $session = New-PSSession -ComputerName $VMIPAddress -Credential $credential `
-SessionOption $sessionOptions -Authentication Basic -ErrorAction Stop -SessionOption $sessionOptions -UseSSL -Port 5986 `
-Authentication Basic -ErrorAction Stop
} }
# ── Post-setup remote validation ────────────────────────────────────────── # ── Post-setup remote validation ──────────────────────────────────────────
@@ -670,12 +660,9 @@ try {
finally { finally {
Remove-PSSession $session -ErrorAction SilentlyContinue Remove-PSSession $session -ErrorAction SilentlyContinue
# Restore host WinRM client settings to their original values # Restore host TrustedHosts to its original value
if ($null -ne $prevAllowUnencrypted) {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -ErrorAction SilentlyContinue
}
if ($null -ne $prevTrustedHosts) { if ($null -ne $prevTrustedHosts) {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue
} }
Write-Host "[Prepare] Host WinRM client settings restored." Write-Host "[Prepare] Host TrustedHosts restored."
} }
File diff suppressed because it is too large Load Diff
+144 -10
View File
@@ -18,9 +18,11 @@
Validation: Domain/Private/Public all Enabled=False Validation: Domain/Private/Public all Enabled=False
Step 2 — Defender: validation only — Deploy set DisableAntiSpyware=1 GPO + RTP off. Step 2 — Defender: validation only — Deploy set DisableAntiSpyware=1 GPO + RTP off.
Validation: GPO DisableAntiSpyware=1 Validation: GPO DisableAntiSpyware=1
Step 3 — WinRM: validation only — Deploy ran Enable-PSRemoting, Basic auth, Step 3 — WinRM: validation only — Deploy ran Enable-PSRemoting, Basic auth over HTTPS,
AllowUnencrypted, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25. AllowUnencrypted=false (HTTPS-only), MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory Validation: service Running+Automatic, AllowUnencrypted=false, Auth/Basic, memory
Step 3b — Windows KMS activation via slmgr /skms + /ato (best-effort, non-fatal).
Validation: LicenseStatus=1 check (warning-only on failure)
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: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1. Step 4b — UAC: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
@@ -161,6 +163,44 @@ function Assert-Step {
Write-Host " [OK] $Description" -ForegroundColor Green Write-Host " [OK] $Description" -ForegroundColor Green
} }
function Assert-Hash {
# Verifies SHA256 of a downloaded file against a pinned expected value.
# If $Expected is empty the check is skipped with a warning (unpinned installer).
# To pin: download the file, run (Get-FileHash <path> -Algorithm SHA256).Hash, fill in below.
param(
[Parameter(Mandatory)] [string] $FilePath,
[Parameter(Mandatory)] [string] $Label,
[string] $Expected = ''
)
if (-not $Expected) {
Write-Host " [WARN] Hash not pinned for $Label — set `$script:Hashes to enforce (see §1.3)" -ForegroundColor Yellow
return
}
$actual = (Get-FileHash $FilePath -Algorithm SHA256).Hash.ToUpper()
if ($actual -ne $Expected.ToUpper()) {
throw "Hash mismatch for ${Label}:`n expected: $($Expected.ToUpper())`n actual: $actual`nPossible MITM or wrong installer version — do NOT proceed."
}
Write-Host " [OK] SHA256 verified: $Label" -ForegroundColor Green
}
# ── Pinned SHA256 hashes for downloaded installers (§1.3) ─────────────────────
# Fill in the hash values below. Get them by:
# (Invoke-WebRequest '<URL>' -UseBasicParsing -OutFile 'tmp.bin'; (Get-FileHash 'tmp.bin' -Algorithm SHA256).Hash)
# Update when the corresponding version param changes. Leave '' to skip (warns at runtime).
$script:Hashes = @{
# python-<PythonVersion>-amd64.exe from https://www.python.org/downloads/release/python-XYZ/
# Update $script:Hashes['Python'] when $PythonVersion changes.
'Python' = ''
# dotnet-install.ps1 from https://dot.net/v1/dotnet-install.ps1
# Changes with each .NET SDK release cycle — verify after any dotnet version bump.
'DotNetInstallScript' = ''
# vs_buildtools.exe bootstrapper from aka.ms/vs/stable/vs_buildtools.exe
# Stable within a VS release cycle — update when VS major version changes.
'VSBuildToolsBootstrapper' = ''
}
# ── Pre-flight: remove temp files from any previous partial run ─────────────── # ── Pre-flight: remove temp files from any previous partial run ───────────────
# These files are normally deleted at the end of each step that creates them. # These files are normally deleted at the end of each step that creates them.
# A previous interrupted run may have left them behind — remove before starting. # A previous interrupted run may have left them behind — remove before starting.
@@ -203,9 +243,9 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
# ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ─────── # ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)" Write-Step "WinRM state (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth, # Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth over HTTPS,
# AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25, # AllowUnencrypted=false (HTTPS/5986 only), TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h,
# and StartupType=Automatic. Validated here before the build user and toolchain steps. # MaxProcesses=25, StartupType=Automatic. Validated here before the build user and toolchain steps.
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'
@@ -213,8 +253,8 @@ Assert-Step 'WinRM' 'service Running' {
Assert-Step 'WinRM' 'service StartType Automatic' { Assert-Step 'WinRM' 'service StartType Automatic' {
(Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic' (Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic'
} }
Assert-Step 'WinRM' 'AllowUnencrypted=true' { Assert-Step 'WinRM' 'AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'false'
} }
Assert-Step 'WinRM' 'Auth/Basic=true' { Assert-Step 'WinRM' 'Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic).Value -eq 'true' (Get-Item WSMan:\localhost\Service\Auth\Basic).Value -eq 'true'
@@ -223,6 +263,33 @@ Assert-Step 'WinRM' 'MaxMemoryPerShellMB >= 2048' {
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB).Value -ge 2048 [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB).Value -ge 2048
} }
# ── Step 3b: Windows KMS Activation ─────────────────────────────────────────
# Must run after network is confirmed (Step 3) and before Windows Update (Step 6)
# so activation completes before WU queries the license state.
# slmgr.vbs is a VBScript — invoke via cscript //NoLogo to get stdout output
# instead of a dialog box (dialogs are invisible / hang in WinRM sessions).
# Best-effort: activation failure does not abort Setup — CI builds work without
# activation (all features used here are available in the grace period).
Write-Step "Windows KMS activation"
$cscript = "$env:SystemRoot\System32\cscript.exe"
$slmgr = "$env:SystemRoot\System32\slmgr.vbs"
Write-Host "Setting KMS server..."
& $cscript //NoLogo $slmgr /skms kms8.msguides.com 2>&1 | Write-Host
Write-Host "Requesting activation..."
& $cscript //NoLogo $slmgr /ato 2>&1 | Write-Host
# Verify activation status (LicenseStatus 1 = Licensed)
$licOk = Get-CimInstance SoftwareLicensingProduct -Filter "Name like 'Windows%'" -ErrorAction SilentlyContinue |
Where-Object { $_.LicenseStatus -eq 1 }
if ($licOk) {
Write-Host " [OK] Windows activated (LicenseStatus=1)." -ForegroundColor Green
} else {
Write-Warning " [WARN] Windows not activated — /ato may have failed or KMS server unreachable. CI builds will work in the grace period."
}
# ── Step 4: Build user account ─────────────────────────────────────────────── # ── Step 4: Build user account ───────────────────────────────────────────────
Write-Step "Creating build user account: $BuildUsername" Write-Step "Creating build user account: $BuildUsername"
@@ -535,6 +602,12 @@ Write-Step "Disabling Windows Update permanently (post-update snapshot lockdown)
# Stop + disable main WU service # Stop + disable main WU service
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
Set-Service -Name wuauserv -StartupType Disabled Set-Service -Name wuauserv -StartupType Disabled
# Write Start=4 directly to the service registry key in addition to the SCM call.
# Set-Service calls ChangeServiceConfig (SCM API) which WaaSMedicSvc can override via its
# own SCM call. The registry Start value requires WaaSMedicSvc to have registry write
# access — denied by the ACL block below.
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' `
-Name Start -Value 4 -Type DWord -Force
# Stop + disable Update Orchestrator Service (UsoSvc) — WU client in Server 2025 # Stop + disable Update Orchestrator Service (UsoSvc) — WU client in Server 2025
Stop-Service -Name UsoSvc -Force -ErrorAction SilentlyContinue Stop-Service -Name UsoSvc -Force -ErrorAction SilentlyContinue
@@ -565,6 +638,30 @@ foreach ($taskName in @('Scheduled Start', 'WakeTimer', 'Automatic App Update',
} }
Write-Host "Windows Update scheduled tasks disabled (best-effort)." Write-Host "Windows Update scheduled tasks disabled (best-effort)."
# Deny WaaSMedicSvc write access to the wuauserv service registry key so it cannot
# reset Start back to Manual (2) or Automatic (3) after we set it to Disabled (4).
# WaaSMedicSvc is a protected service that cannot itself be disabled; ACL denial
# is the only reliable way to prevent it from healing the service startup type.
# Best-effort: wrapped in try/catch so that a WinRM session permission failure is
# non-fatal (the GPO keys + Start=4 still provide a good baseline).
try {
$svcRegPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv'
$acl = Get-Acl $svcRegPath
$medicSid = [System.Security.Principal.NTAccount]'NT SERVICE\WaaSMedicSvc'
$denyRule = New-Object System.Security.AccessControl.RegistryAccessRule(
$medicSid,
[System.Security.AccessControl.RegistryRights]'SetValue,CreateSubKey,WriteKey',
[System.Security.AccessControl.InheritanceFlags]::None,
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Deny
)
$acl.AddAccessRule($denyRule)
Set-Acl $svcRegPath $acl
Write-Host "wuauserv: WaaSMedicSvc write-deny ACL applied."
} catch {
Write-Warning "wuauserv ACL deny skipped (non-fatal — GPO keys + Start=4 still in place): $_"
}
# Validation # Validation
Assert-Step 'WUDisable' 'wuauserv StartType Disabled' { Assert-Step 'WUDisable' 'wuauserv StartType Disabled' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled' (Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
@@ -589,6 +686,8 @@ else {
$dotnetInstallScript = "$env:TEMP\dotnet-install.ps1" $dotnetInstallScript = "$env:TEMP\dotnet-install.ps1"
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' ` Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' `
-OutFile $dotnetInstallScript -UseBasicParsing -OutFile $dotnetInstallScript -UseBasicParsing
Assert-Hash -FilePath $dotnetInstallScript -Label 'dotnet-install.ps1' `
-Expected $script:Hashes['DotNetInstallScript']
Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..." Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..."
& $dotnetInstallScript ` & $dotnetInstallScript `
@@ -635,6 +734,8 @@ else {
$pyInstallerPath = 'C:\CI\python_installer.exe' $pyInstallerPath = 'C:\CI\python_installer.exe'
Write-Host "Downloading Python $PythonVersion installer..." Write-Host "Downloading Python $PythonVersion installer..."
Invoke-WebRequest -Uri $pyInstallerUrl -OutFile $pyInstallerPath -UseBasicParsing Invoke-WebRequest -Uri $pyInstallerUrl -OutFile $pyInstallerPath -UseBasicParsing
Assert-Hash -FilePath $pyInstallerPath -Label "python-$PythonVersion-amd64.exe" `
-Expected $script:Hashes['Python']
Write-Host "Installing Python $PythonVersion (all users, prepend PATH)..." Write-Host "Installing Python $PythonVersion (all users, prepend PATH)..."
$pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @( $pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @(
@@ -689,6 +790,8 @@ else {
Write-Host "Downloading VS Build Tools installer..." Write-Host "Downloading VS Build Tools installer..."
Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing
Assert-Hash -FilePath $vsInstallerPath -Label 'vs_buildtools.exe' `
-Expected $script:Hashes['VSBuildToolsBootstrapper']
# Remove Zone.Identifier ADS — files downloaded from internet are marked Zone 3 (Internet). # Remove Zone.Identifier ADS — files downloaded from internet are marked Zone 3 (Internet).
# SYSTEM account cannot execute them without this unblock step. # SYSTEM account cannot execute them without this unblock step.
@@ -947,6 +1050,8 @@ Write-Host "Disk cleanup complete."
foreach ($svc in 'wuauserv', 'UsoSvc') { foreach ($svc in 'wuauserv', 'UsoSvc') {
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
} }
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' `
-Name Start -Value 4 -Type DWord -Force -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.
@@ -955,14 +1060,39 @@ foreach ($svc in 'wuauserv', 'UsoSvc') {
# normal and harmless: WU is permanently disabled by Step 6b (service Disabled + GPO keys). # normal and harmless: WU is permanently disabled by Step 6b (service Disabled + GPO keys).
# Report remaining size as informational only. # Report remaining size as informational only.
$sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -File -Force -ErrorAction SilentlyContinue $sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -File -Force -ErrorAction SilentlyContinue
$sdMB = [math]::Round(($sdFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 1) # Avoid Measure-Object .Sum: under Set-StrictMode -Version Latest, PS 5.1 treats the
Write-Host " SoftwareDistribution\Download: $($sdFiles.Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)." # nullable Sum property as absent when the pipe is empty, throwing "property not found".
$sdBytes = [long]0
if ($sdFiles) { foreach ($f in $sdFiles) { $sdBytes += $f.Length } }
$sdMB = [math]::Round($sdBytes / 1MB, 1)
Write-Host " SoftwareDistribution\Download: $(@($sdFiles).Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
Assert-Step 'Cleanup' 'wuauserv StartType 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)
# ── Pre-Final re-affirmation ──────────────────────────────────────────────────
# Steps 7-10 (installs, cleanup, DISM) can take hours. During that window background
# tasks or WaaSMedicSvc may reset AutoAdminLogon or wuauserv. Re-affirm both here
# immediately before the Final gate so the snapshot captures clean state.
$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
}
if ($AdminPassword -ne '') {
Set-ItemProperty -Path $wlKey -Name DefaultPassword -Value $AdminPassword -Type String -Force
}
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
Set-Service -Name wuauserv -StartupType Disabled
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' `
-Name Start -Value 4 -Type DWord -Force
# ── Final pre-snapshot validation ──────────────────────────────────────────── # ── Final pre-snapshot validation ────────────────────────────────────────────
Write-Step "Final pre-snapshot validation" Write-Step "Final pre-snapshot validation"
@@ -984,6 +1114,10 @@ Assert-Step 'Final' 'dotnet, python, msbuild all resolvable' {
(Test-Path 'C:\Python\python.exe' -PathType Leaf) -and (Test-Path 'C:\Python\python.exe' -PathType Leaf) -and
(Test-Path $msbuildPath -PathType Leaf) (Test-Path $msbuildPath -PathType Leaf)
} }
Assert-Step 'Final' 'AutoAdminLogon=1, DefaultUserName=Administrator' {
$wl = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
}
Assert-Step 'Final' 'Windows Update permanently disabled' { Assert-Step 'Final' 'Windows Update permanently disabled' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled' (Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
} }
+4 -7
View File
@@ -42,11 +42,9 @@ $cred = New-Object System.Management.Automation.PSCredential(
) )
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try { try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -56,7 +54,7 @@ try {
Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
$results = [System.Collections.Generic.List[PSCustomObject]]::new() $results = [System.Collections.Generic.List[PSCustomObject]]::new()
@@ -82,8 +80,8 @@ try {
# ── WinRM ───────────────────────────────────────────────────────────── # ── WinRM ─────────────────────────────────────────────────────────────
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } 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 service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false'
} }
Chk 'WinRM Auth/Basic=true' { Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
@@ -182,6 +180,5 @@ try {
} }
} finally { } finally {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
} }
+7 -10
View File
@@ -63,11 +63,9 @@ $cred = New-Object System.Management.Automation.PSCredential(
) )
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck $so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try { try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) { if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress } $newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -77,7 +75,7 @@ try {
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion) param($BuildUsername, $DotNetChannel, $PythonVersion)
@@ -109,8 +107,8 @@ try {
# WinRM # WinRM
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' } 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 service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { Chk 'WinRM AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false'
} }
Chk 'WinRM Auth/Basic=true' { Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true' (Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
@@ -270,7 +268,7 @@ try {
Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow
Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param([string[]] $FailedNames, [string] $AdminPassword) param([string[]] $FailedNames, [string] $AdminPassword)
@@ -296,7 +294,7 @@ try {
# ── Re-run checks ───────────────────────────────────────────────────────── # ── Re-run checks ─────────────────────────────────────────────────────────
Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred ` $checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock { -Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion) param($BuildUsername, $DotNetChannel, $PythonVersion)
$results = [System.Collections.Generic.List[PSCustomObject]]::new() $results = [System.Collections.Generic.List[PSCustomObject]]::new()
@@ -310,7 +308,7 @@ try {
Chk 'Defender GPO DisableAntiSpyware=1' { (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' -Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1 } 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 Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' } 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 AllowUnencrypted=false (HTTPS-only)' { (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'false' }
Chk 'WinRM Auth/Basic=true' { (Get-Item WSMan:\localhost\Service\Auth\Basic -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 } Chk 'WinRM MaxMemoryPerShellMB >= 2048' { [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048 }
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
@@ -369,6 +367,5 @@ try {
} }
} finally { } finally {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
} }