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`
- act_runner v1.0.2 — Windows service su host, label `windows-build`
- 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
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 |
| Python | 3.13.3 |
| 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
WinRM in modalità HTTP/Basic è **accettabile solo in ambiente lab isolato**.
Per ambienti di produzione o condivisi, migrare a HTTPS/5986 con certificato self-signed
e rimuovere `AllowUnencrypted=true`. Vedi [docs/BEST-PRACTICES.md](docs/BEST-PRACTICES.md).
WinRM usa **HTTPS/5986** con certificato self-signed e Basic auth.
`-SkipCACheck`/`-SkipCNCheck` sono accettabili in lab isolato (il cert non è di una CA trusted).
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
---
## 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
- [x] **Gitea Server** — già in esecuzione su `http://10.10.20.11:3100` (e `https://gitea.emulab.it`)
@@ -65,7 +93,7 @@
```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck
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
```
- [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.
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
- Guest prep: `C:\CI` esiste, file copiato, size match
- Post-setup remoto (9 check): WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs
@@ -148,25 +176,22 @@
## 1. Sicurezza & hardening
### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986
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.
### 1.1 [P0] [x] Migrare WinRM da HTTP/Basic a HTTPS/5986 — COMPLETATO 2026-05-10
**Azioni**:
- [ ] Generare cert self-signed nel template *prima* dello snapshot.
- [ ] Sostituire `New-PSSession -ComputerName $ip` con `-UseSSL -Port 5986` in `Invoke-RemoteBuild.ps1` e `Get-BuildArtifacts.ps1`.
- [ ] Rimuovere `AllowUnencrypted=true` dal `Setup-WinBuild2025.ps1`.
- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline.
- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT).
- [x] Generare cert self-signed nel template *prima* dello snapshot (già in Deploy post-install.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`.
- [x] Rimuovere `AllowUnencrypted=true` da Deploy post-install.ps1 e Setup-WinBuild2025.ps1 (assertion → false).
- [x] `-SkipCACheck`/`-SkipCNCheck`/`-SkipRevocationCheck` mantenuti nei `New-PSSessionOption` e `New-WSManSessionOption` (cert lab self-signed — documentato inline).
- [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:
- `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
**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
```
### 1.3 [P0] Pinning hash SHA256 degli installer
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).
### 1.3 [P0] [ ] Pinning hash SHA256 degli installer — STRUTTURA AGGIUNTA 2026-05-10 (valori da riempire)
**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza
verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari
compromessi nel template, che poi viene snapshottato e usato da *ogni* build.
`$script:Hashes` + `Assert-Hash` aggiunti a [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
`Assert-Hash` viene chiamato dopo ogni download (dotnet-install.ps1, python installer, vs_buildtools.exe).
Se l'hash è `''` viene emesso un warning ma l'esecuzione continua (non-breaking).
Pattern minimale:
```powershell
$expected = '6F25A7DF...' # SHA256 pinnato per la versione esatta
$actual = (Get-FileHash $pyInstallerPath -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected.ToLower()) { throw "Hash mismatch: $actual" }
```
Aggiungere un blocco hash per ogni download e bloccarli in costanti `$script:Hashes`.
Aggiornare al cambio versione (è raro).
**Azione residua — OBBLIGATORIA prima del prossimo refresh snapshot**:
- [ ] Scaricare i tre file, calcolare hash, riempire `$script:Hashes` in Setup-WinBuild2025.ps1:
```powershell
# Inside or outside VM — qualsiasi PowerShell:
(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']
(Get-FileHash 'vs_buildtools.exe' -Algorithm SHA256).Hash # → $script:Hashes['VSBuildToolsBootstrapper']
```
- [ ] Verificare che `Assert-Hash` throwi correttamente su hash errato (test con hash fake).
### 1.4 [P1] Validazione IP per-ottetto
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).
### 1.4 [P1] [x] Validazione IP per-ottetto — COMPLETATO 2026-05-10
**Motivazione**: `'^(\d{1,3}\.){3}\d{1,3}$'` accetta `999.999.999.999`. Sostituire con regex
per-ottetto:
```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).
Regex per-ottetto sostituita in tutti e 4 i file:
`'^((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)$'`
### 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
(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,
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).
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 tracciamento.
### 1.7 [P2] Rotazione password guest VM
### 1.7 [P2] [ ] Rotazione password guest VM
- [ ] Trimestrale. Aggiornare credenziale `BuildVMGuest` in Credential Manager + password
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
lo usano (post-migrazione HTTPS).
@@ -245,7 +266,7 @@ senza tracciamento.
## 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).
**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).
### 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.
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.
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.
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,
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).
Aggiungere `[CmdletBinding(SupportsShouldProcess)]` come fatto in `Cleanup-OrphanedBuildVMs.ps1`.
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).
**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).
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.
Pre-snapshot: copia atomica del `parent` VMDK in `F:\CI\Backups\Template_<date>\`. Su errore
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.
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.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).
**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.
### 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`).
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`.
### 3.3 [P2] In-VM clone (`-UseGitClone`)
### 3.3 [P2] [ ] In-VM clone (`-UseGitClone`)
Implementazione disegnata di seguito (sezione "In-VM Git Clone (Ottimizzazione)").
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.
### 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).
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
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.
`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.
- 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).
---
## 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).
**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
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:
```
ci_runner_orphan_vms 0
@@ -434,13 +455,13 @@ ci_runner_active_jobs 1
Se l'homelab ha già Prometheus + node_exporter, basta
`--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`.
`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.
### 4.4 [P2] Runbook per incident comuni
### 4.4 [P2] [ ] Runbook per incident comuni
File: nuovo `docs/RUNBOOK.md`.
Documentare con copy-pasta:
@@ -451,7 +472,7 @@ Documentare con copy-pasta:
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`)
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.
@@ -460,7 +481,7 @@ file:// — utile per debug a colpo d'occhio senza aprire Gitea UI.
## 5. Qualità codice & test
### 5.1 [P1] Pester smoke tests sugli script
### 5.1 [P1] [ ] Pester smoke tests sugli script
File: nuovo `tests/` directory.
**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).
### 5.2 [P2] Modulo PowerShell condiviso `scripts/_Common.psm1`
### 5.2 [P2] [ ] Modulo PowerShell condiviso `scripts/_Common.psm1`
**Duplicazioni da estrarre**:
- `New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck` (3 file).
- `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.
### 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`:
```powershell
$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
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).
Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
@@ -502,7 +523,7 @@ Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
- `PSUseShouldProcessForStateChangingFunctions`.
- 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
validazione runtime.
@@ -510,7 +531,7 @@ validazione runtime.
## 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)
— 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
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).
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.
### 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
stesso `Setup-Host.ps1` registrato come secondo runner Gitea (label `windows-build:host-2`).
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
(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).
Attualmente single job. Quando arriverà la VM Linux:
@@ -557,11 +578,11 @@ strategy:
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
`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).
**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,
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),
[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] 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),
[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] 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/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
### 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] 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
- 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 —
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
+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_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
- Default transport for remote build execution inside VMs
- Port: **5985** (HTTP, in-lab use); migrate to 5986/HTTPS for hardened setups
- Authentication: **Basic** (unencrypted — acceptable for isolated lab)
- Port: **5986** (HTTPS, self-signed cert, `AllowUnencrypted=false`)
- Authentication: **Basic over HTTPS** (`-SkipCACheck` for lab self-signed cert)
- Used for:
- Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest
- Running build commands (`Invoke-Command`)
@@ -133,7 +133,7 @@ Build VMs:
...
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.
```
+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:
- Build VMs sono su VMnet8 NAT (192.168.79.0/24) — raggiungibili dall'host, internet via NAT
- No external traffic reaches port 5985
- Credentials are managed via Windows Credential Manager
`Deploy-WinBuild2025.ps1` post-install.ps1 crea un certificato self-signed e configura
il listener HTTPS/5986 **prima** dello snapshot `BaseClean`. `AllowUnencrypted=false`.
### 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
# 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
$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL `
$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL -Authentication Basic `
-Credential $cred -SessionOption $sessionOptions
```
> `-SkipCACheck` is acceptable for a self-signed cert in an isolated lab. Do NOT
> use this against externally accessible machines.
> `-SkipCACheck`/`-SkipCNCheck` sono accettabili per un cert self-signed in lab isolato.
> 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:
- 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)
- 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):
```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
```
*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)
+1 -1
View File
@@ -314,7 +314,7 @@ Equivalente di `Prepare-WinBuild2025.ps1` per Linux. Usa SSH invece di WinRM.
| 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 |
| Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` |
| Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` |
+9 -8
View File
@@ -65,7 +65,8 @@ winrm quickconfig -q
```powershell
Enable-PSRemoting -Force -SkipNetworkProfileCheck
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
```
@@ -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):
1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5985 reachable
2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts`
- Salva valori precedenti, ripristinati nel `finally` (host posture invariata)
3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP
4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin
5. **Apre PSSession** verso la VM
1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5986 reachable
2. **Configura host WinRM client**: aggiunge `$VMIPAddress` ai `TrustedHosts` (no `AllowUnencrypted` — HTTPS non lo richiede)
- Salva valore precedente, ripristinato nel `finally` (host posture invariata)
3. **Validazione host WinRM**: `TrustedHosts` include IP
4. **Test connettività**: `Test-WSMan -Port 5986 -UseSSL -Authentication Basic` con credenziali admin
5. **Apre PSSession** `-UseSSL -Port 5986` verso la VM
6. **Crea `C:\CI`** in guest, valida creazione
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)
@@ -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` |
| 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 |
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Operativo | Ogni dir Test-Path -PathType Container |
+8 -5
View File
@@ -39,7 +39,7 @@
[CmdletBinding()]
param(
[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,
[Parameter(Mandatory)]
@@ -67,10 +67,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..."
$session = New-PSSession `
-ComputerName $IPAddress `
-Credential $Credential `
-SessionOption $sessionOptions `
-ErrorAction Stop
-ComputerName $IPAddress `
-Credential $Credential `
-SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try {
# ── Verify artifact exists in guest ──────────────────────────────────
+1 -1
View File
@@ -103,7 +103,7 @@ param(
[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] $GuestCredentialTarget = 'BuildVMGuest',
+8 -5
View File
@@ -49,7 +49,7 @@
[CmdletBinding()]
param(
[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,
[Parameter(Mandatory)]
@@ -85,10 +85,13 @@ $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationC
Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..."
$session = New-PSSession `
-ComputerName $IPAddress `
-Credential $Credential `
-SessionOption $sessionOptions `
-ErrorAction Stop
-ComputerName $IPAddress `
-Credential $Credential `
-SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try {
# ── Ensure working directories exist on guest ────────────────────────
+11 -8
View File
@@ -40,7 +40,7 @@ param(
[string] $VMPath,
[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,
[ValidateRange(30, 600)]
@@ -52,7 +52,7 @@ param(
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
# 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.
[switch] $SkipPing
)
@@ -107,17 +107,20 @@ while ((Get-Date) -lt $deadline) {
}
}
# ── Phase 3: WinRM responsive ────────────────────────────────────────
try {
Test-WSMan -ComputerName $IPAddress -ErrorAction Stop | Out-Null
# Success — VM is ready
# ── Phase 3: WinRM port 5986 accepting TCP connections ───────────────
# Test-WSMan has no -SessionOption in PS 5.1 and fails cert validation for
# self-signed certs. TCP open on 5986 = HTTPS listener up = VM 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)."
return
}
catch {
else {
$phase = 'winrm'
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
}
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)
- Windows Firewall all profiles disabled (VMnet8 NAT lab; Setup Step 1 validates)
- RDP enabled (no NLA, lab only) + firewall rules for RDP/ICMP/WinRM-HTTPS
- WinRM HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth,
AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25
- WinRM HTTPS 5986 only (self-signed cert), Basic auth, AllowUnencrypted=false,
TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25
- ICMPv4 inbound allowed
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
@@ -127,7 +127,7 @@
[CmdletBinding()]
param(
# ── 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',
# 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
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
winrm set winrm/config/service '@{AllowUnencrypted="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 '@{TrustedHosts="*"}' | Out-Null
try {
`$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-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null
L 'WinRM HTTPS listener configured'
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 (port 5986, restricted to 192.168.79.0/24)'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null
@@ -1107,8 +1107,7 @@ Write-Host " Admin user : $AdminUser"
Write-Host " Snapshot : $SnapshotName"
Write-Host ""
Write-Host " RDP : mstsc /v:$guestIP"
Write-Host " WinRM HTTP : 5985"
Write-Host " WinRM HTTPS : 5986 (self-signed)"
Write-Host " WinRM HTTPS : 5986 (self-signed, AllowUnencrypted=false)"
Write-Host ""
Write-Host " Next steps (from template\ dir):"
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
Automates the template VM provisioning from the host machine:
1. Pre-flight validation: script presence, IP octet range, TCP/5985 reachable
2. Configures host WinRM client (AllowUnencrypted, TrustedHosts) — restored on exit
3. Validates host WinRM client settings applied correctly
4. Tests WinRM connectivity (Test-WSMan) — fails fast with actionable message
5. Opens PSSession to the VM
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-WinBuild2025.ps1 into the VM; validates file present + size match
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
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 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.
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)
$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)
$winrmReady = $false
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) {
$winrmReady = $true; break
}
Start-Sleep -Seconds 5
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.
if ($BuildPassword -eq '') {
@@ -261,59 +262,53 @@ else {
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
# ── Configure HOST-side WinRM client (required for Basic auth over HTTP) ──────
# AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value
# and restore it in the finally block so this script doesn't permanently change
# the host security posture.
Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..."
$prevAllowUnencrypted = $null
$prevTrustedHosts = $null
# ── 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 {
$prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop
$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 WinRM client configured."
# Validation
Assert-Step 'HostWinRM' 'Client/AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value -eq 'true'
}
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 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 " Set-Item WSMan:\localhost\Client\AllowUnencrypted `$true -Force"
Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force"
Write-Warning "Then re-run this script."
exit 1
}
Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..."
try {
Test-WSMan -ComputerName $VMIPAddress -Credential $credential `
-Authentication Basic -ErrorAction Stop | Out-Null
Write-Host "[Prepare] WinRM reachable."
}
catch {
# ── 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 reach $VMIPAddress via WinRM.
[Prepare] Cannot open PSSession to $VMIPAddress via WinRM HTTPS (port 5986).
Ensure:
- The VM is running and on VMnet8 (NAT) with internet access
- WinRM is enabled inside the VM (run as Administrator in VM):
winrm quickconfig -q
Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
- Windows Firewall allows port 5985 inbound
- WinRM HTTPS is enabled inside the VM Deploy-WinBuild2025.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: $_
@@ -321,15 +316,6 @@ Error: $_
exit 1
}
# ── Open session ──────────────────────────────────────────────────────────────
Write-Host "[Prepare] Opening PSSession..."
$session = New-PSSession `
-ComputerName $VMIPAddress `
-Credential $credential `
-SessionOption $sessionOptions `
-Authentication Basic `
-ErrorAction Stop
try {
# ── Copy Setup-WinBuild2025.ps1 into the VM ───────────────────────────────
$setupScript = Join-Path $scriptDir 'Setup-WinBuild2025.ps1'
@@ -423,9 +409,12 @@ try {
while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 10
try {
$r = Test-WSMan -ComputerName $IP -Credential $Cred `
-Authentication Basic -ErrorAction Stop
if ($r) { return $true }
# 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
@@ -481,7 +470,8 @@ try {
}
Write-Host "[Prepare] Guest WinRM ready, reopening session..."
$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 ──────────────────────────────────────────
@@ -670,12 +660,9 @@ try {
finally {
Remove-PSSession $session -ErrorAction SilentlyContinue
# Restore host WinRM client settings to their original values
if ($null -ne $prevAllowUnencrypted) {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -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 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
Step 2 Defender: validation only Deploy set DisableAntiSpyware=1 GPO + RTP off.
Validation: GPO DisableAntiSpyware=1
Step 3 WinRM: validation only Deploy ran Enable-PSRemoting, Basic auth,
AllowUnencrypted, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory
Step 3 WinRM: validation only Deploy ran Enable-PSRemoting, Basic auth over HTTPS,
AllowUnencrypted=false (HTTPS-only), MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25.
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)
Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b UAC: validation only Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
@@ -161,6 +163,44 @@ function Assert-Step {
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 ───────────────
# 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.
@@ -203,9 +243,9 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
# ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth,
# AllowUnencrypted, TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h, MaxProcesses=25,
# and StartupType=Automatic. Validated here before the build user and toolchain steps.
# Deploy-WinBuild2025.ps1 post-install.ps1 ran Enable-PSRemoting, set Basic auth over HTTPS,
# AllowUnencrypted=false (HTTPS/5986 only), TrustedHosts=*, MaxMemory=2048MB, IdleTimeout=2h,
# MaxProcesses=25, StartupType=Automatic. Validated here before the build user and toolchain steps.
Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
@@ -213,8 +253,8 @@ Assert-Step 'WinRM' 'service Running' {
Assert-Step 'WinRM' 'service StartType Automatic' {
(Get-Service WinRM -ErrorAction Stop).StartType -eq 'Automatic'
}
Assert-Step 'WinRM' 'AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'true'
Assert-Step 'WinRM' 'AllowUnencrypted=false (HTTPS-only)' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted).Value -eq 'false'
}
Assert-Step 'WinRM' 'Auth/Basic=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
}
# ── 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 ───────────────────────────────────────────────
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-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
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-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)."
# 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
Assert-Step 'WUDisable' 'wuauserv StartType Disabled' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
@@ -589,6 +686,8 @@ else {
$dotnetInstallScript = "$env:TEMP\dotnet-install.ps1"
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' `
-OutFile $dotnetInstallScript -UseBasicParsing
Assert-Hash -FilePath $dotnetInstallScript -Label 'dotnet-install.ps1' `
-Expected $script:Hashes['DotNetInstallScript']
Write-Host "Installing .NET SDK $DotNetSdkVersion (channel)..."
& $dotnetInstallScript `
@@ -635,6 +734,8 @@ else {
$pyInstallerPath = 'C:\CI\python_installer.exe'
Write-Host "Downloading Python $PythonVersion installer..."
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)..."
$pyProc = Start-Process -FilePath $pyInstallerPath -ArgumentList @(
@@ -689,6 +790,8 @@ else {
Write-Host "Downloading VS Build Tools installer..."
Remove-Item $vsInstallerPath -ErrorAction SilentlyContinue # remove any stale/corrupt copy
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).
# SYSTEM account cannot execute them without this unblock step.
@@ -947,6 +1050,8 @@ Write-Host "Disk cleanup complete."
foreach ($svc in 'wuauserv', 'UsoSvc') {
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
# 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).
# Report remaining size as informational only.
$sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -File -Force -ErrorAction SilentlyContinue
$sdMB = [math]::Round(($sdFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 1)
Write-Host " SoftwareDistribution\Download: $($sdFiles.Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
# Avoid Measure-Object .Sum: under Set-StrictMode -Version Latest, PS 5.1 treats the
# 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' {
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
}
} # 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 ────────────────────────────────────────────
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 $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' {
(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
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -56,7 +54,7 @@ try {
Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$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()
@@ -82,8 +80,8 @@ try {
# ── WinRM ─────────────────────────────────────────────────────────────
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
Chk 'WinRM 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'
@@ -182,6 +180,5 @@ try {
}
} 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
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
@@ -77,7 +75,7 @@ try {
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion)
@@ -109,8 +107,8 @@ try {
# WinRM
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
Chk 'WinRM 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'
@@ -270,7 +268,7 @@ try {
Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow
Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param([string[]] $FailedNames, [string] $AdminPassword)
@@ -296,7 +294,7 @@ try {
# ── Re-run checks ─────────────────────────────────────────────────────────
Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion)
$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 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' { (Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true' }
Chk 'WinRM 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 MaxMemoryPerShellMB >= 2048' { [int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048 }
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
@@ -369,6 +367,5 @@ try {
}
} 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
}