refactor(template): §7 — move OS hardening to Deploy, Setup becomes validation-only

§7.1 — Setup Steps 1/3/4b/5b/5c converted to Assert-Step only:
  - Deploy post-install.ps1 now owns: Firewall (all profiles disabled),
    WinRM MaxMemory=2048MB/IdleTimeout=2h/MaxProcesses=25/StartupType=Automatic,
    lock screen (NoLockScreen), SM GPO policy key + scheduled task disable,
    DisableCAD in Winlogon, OOBE non-policy key.
  - Setup no longer re-applies these — validates them, throws if Deploy missed any.

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-10 01:27:29 +02:00
parent ea882c99d7
commit 260e275b89
4 changed files with 199 additions and 265 deletions
+41 -34
View File
@@ -570,18 +570,18 @@ build più ampi senza toccare il template.
Tool da aggiungere come step `Setup-WinBuild2025` (ognuno con `Assert-Step`):
| Tool | Versione | Razionale |
| ------------------- | ----------- | ------------------------------------------------------------ |
| **Git for Windows** | latest LTS | Self-clone in VM (alt a host-zip-transfer), submodule fetch |
| **7-Zip** | latest | Universale unpack/zip; molti installer e workflow ne dipendono |
| **PowerShell 7.x** | latest LTS | Più veloce di 5.1, parallel pipelines, operatori moderni |
| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) |
| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi |
| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) |
| **WiX Toolset** | v4 stable | MSI building da .NET projects |
| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR |
| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky |
| **vcpkg** | latest | Package manager C++ per dipendenze native |
| Tool | Versione | Razionale |
| ------------------- | ----------------------- | -------------------------------------------------------------- |
| **Git for Windows** | latest LTS | Self-clone in VM (alt a host-zip-transfer), submodule fetch |
| **7-Zip** | latest | Universale unpack/zip; molti installer e workflow ne dipendono |
| **PowerShell 7.x** | latest LTS | Più veloce di 5.1, parallel pipelines, operatori moderni |
| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) |
| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi |
| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) |
| **WiX Toolset** | v4 stable | MSI building da .NET projects |
| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR |
| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky |
| **vcpkg** | latest | Package manager C++ per dipendenze native |
**Approccio**:
- Pinning hash SHA256 per ogni installer (vedi §1.3)
@@ -619,27 +619,33 @@ 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] Spostare base OS hardening da Setup a Deploy ✅ codice fatto 2026-05-10
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
**Implementato**: Firewall disable + WinRM MaxMemory/IdleTimeout/MaxProcesses spostati in
Deploy post-install.ps1. Aggiunti anche lock screen, SM GPO policy key, SM scheduled task
disable, DisableCAD Winlogon, OOBE non-policy key (mancanti vs Setup 5c).
Setup Step 1/3/4b/5b/5c convertiti a Assert-Step only — nessun Set.
Pending §7.4 per e2e validation su VM reale.
**Da MUOVERE Setup → Deploy**:
| Concern Setup | Step attuale Setup | Azione Deploy |
| ----------------------------- | ------------------ | ------------------------------------------- |
| Firewall off all profiles | Step 1 | Aggiungere `Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False` in post-install (ora Deploy aggiunge solo regole RDP/ICMP) |
| WinRM tuning memoria/timeout | Step 3 (parziale) | Aggiungere `MaxMemoryPerShellMB=2048`, `IdleTimeOut=7200000`, `MaxProcessesPerShell=25` in post-install dopo Enable-PSRemoting |
| Concern Setup | Step attuale Setup | Azione Deploy |
| ---------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Firewall off all profiles | Step 1 | Aggiungere `Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False` in post-install (ora Deploy aggiunge solo regole RDP/ICMP) |
| WinRM tuning memoria/timeout | Step 3 (parziale) | Aggiungere `MaxMemoryPerShellMB=2048`, `IdleTimeOut=7200000`, `MaxProcessesPerShell=25` in post-install dopo Enable-PSRemoting |
**Da CONVERTIRE in Setup a Assert-Step (validation-only)**:
| Step Setup | Diventa |
| ---------- | ------- |
| Step 1 Firewall | `Assert-Step` 3 profili Enabled=False (richiede prima la riga "Da MUOVERE" → Deploy: Set-NetFirewallProfile -Enabled False) |
| Step 2 Defender | Già validation-only (refactor 2026-05-09) |
| Step 3 WinRM tuning | `Assert-Step` MaxMemoryPerShellMB, IdleTimeOut, AllowUnencrypted, Basic |
| Step 4b UAC | `Assert-Step` EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| Step 5b Explorer LaunchTo | `Assert-Step` HKCU LaunchTo=1 |
| Step 5c Server Manager / DisableCAD / OOBE | `Assert-Step` HKLM machine policy + DisableCAD=1 + OOBE policy |
| Step Setup | Diventa |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| Step 1 Firewall | `Assert-Step` 3 profili Enabled=False (richiede prima la riga "Da MUOVERE" → Deploy: Set-NetFirewallProfile -Enabled False) |
| Step 2 Defender | Già validation-only (refactor 2026-05-09) |
| Step 3 WinRM tuning | `Assert-Step` MaxMemoryPerShellMB, IdleTimeOut, AllowUnencrypted, Basic |
| Step 4b UAC | `Assert-Step` EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| Step 5b Explorer LaunchTo | `Assert-Step` HKCU LaunchTo=1 |
| Step 5c Server Manager / DisableCAD / OOBE | `Assert-Step` HKLM machine policy + DisableCAD=1 + OOBE policy |
**Da MANTENERE in Setup (build env, non base OS)**:
- Step 4: build user `ci_build` + admin
@@ -653,10 +659,15 @@ File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
- Setup run dopo Deploy: tutte Assert-Step pass `[OK]` immediato (no work)
- Test `--Skip` orthogonali (no regressione)
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle)
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle) ✅ codice fatto 2026-05-10
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
**Implementato**: Deploy sostituisce `sc.exe config start= disabled` con
`NoAutoUpdate=1` + `DisableWindowsUpdateAccess=1` GPO (servizi rimangono Manual).
Setup Step 6 Step A/D già gestisce reset + restart servizi (idempotente).
Pending §7.4 per e2e validation.
**Stato attuale (conflitto ordering)**:
- Deploy: `sc.exe config wuauserv start= disabled` + `sc.exe stop wuauserv` (idem UsoSvc, WaaSMedicSvc) + GPO `NoAutoUpdate=1`
- Setup Step 6: necessita servizi UP per SchTask SYSTEM run WU
@@ -686,14 +697,10 @@ File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
- Setup `-SkipWindowsUpdate`: Final gate vede `StartType=Disabled` (impostato da Step 6b)
- Setup senza `-Skip`: WU runs, exit 0/2/3, poi servizi disabled
### 7.3 [P2] Update header docs di entrambi gli script
- [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1) `.DESCRIPTION`:
enumerare tutte le hardening voci che Deploy ora possiede (firewall, WinRM tuning,
WU GPO non disable)
- [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1) `.DESCRIPTION`:
marcare Step 1/3/4b/5b/5c come "validation-only — set by Deploy"
- [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): aggiornare descrizione
"Cosa fa Setup-WinBuild2025" — riflettere nuovo confine
### 7.3 [P2] Update header docs di entrambi gli script ✅ fatto 2026-05-10
- [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).
- [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.
- [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): architettura "tre script", VMX path corretti, Fase C step list + razionale + tabella validazioni aggiornati.
### 7.4 [P2] Test e2e refactor
- Run Deploy stand-alone su VM test → verifica tutte le hardening voci presenti
+49 -48
View File
@@ -9,12 +9,17 @@ credenziali archiviate, ISO Windows Server presente).
---
## Architettura: due script
## Architettura: tre script
| Script | Dove gira | Scopo |
| ---------------------------------------- | --------------- | -------------------------------------------------- |
| `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue lo script in VM |
| `template/Setup-WinBuild2025.ps1` | **Dentro VM** | Provisioning effettivo (firewall, defender, tool) |
| Script | Dove gira | Scopo |
| ---------------------------------------- | --------------- | ------------------------------------------------------------------- |
| `template/Deploy-WinBuild2025.ps1` | **Host** | Opzionale: crea VM e installa Windows unattended da ISO |
| `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue Setup nel guest via WinRM |
| `template/Setup-WinBuild2025.ps1` | **Dentro VM** | CI toolchain (user, .NET, Python, VS) + validazione hardening Deploy |
**Confine Deploy / Setup** (refactor §7, 2026-05-10):
- `Deploy-WinBuild2025.ps1` si occupa dell'**OS hardening** (Firewall, WinRM, UAC, Defender, UX tweaks, WU GPO locks). Se usato, Setup valida questi come Assert-Step senza re-applicarli.
- `Setup-WinBuild2025.ps1` si occupa della **CI customization** (utente `ci_build`, cartelle `C:\CI`, toolchain .NET/Python/VS, Windows Update lifecycle, cleanup, final gate).
`Prepare-WinBuild2025.ps1` apre una WinRM session, copia `Setup-WinBuild2025.ps1` in
`C:\CI\`, lo esegue, raccoglie l'exit code, valida lo stato del guest.
@@ -30,7 +35,7 @@ credenziali archiviate, ISO Windows Server presente).
| RAM | 6144 MB (6 GB) |
| Disco | 80 GB thin provisioned |
| NIC | **VMnet8 (NAT)** — internet richiesto per WU + installer |
| VMX path | `F:\CI\Templates\WinBuild\WinBuild.vmx` |
| VMX path | `F:\CI\Templates\WinBuild\CI-WinBuild.vmx` |
| ISO | Windows Server 2025 (da `F:\CI\ISO\`) |
---
@@ -41,7 +46,7 @@ credenziali archiviate, ISO Windows Server presente).
- File → New Virtual Machine → Custom
- Hardware: 4 vCPU, 6144 MB, 80 GB thin
- Network: **VMnet8 (NAT)**
- VMX: `F:\CI\Templates\WinBuild\WinBuild.vmx`
- VMX: `F:\CI\Templates\WinBuild\CI-WinBuild.vmx`
- CD/DVD: monta ISO Windows Server 2025
### A.2 Installa Windows Server 2025
@@ -142,21 +147,22 @@ Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass):
## Fase C — Cosa fa `Setup-WinBuild2025.ps1` (dentro la VM)
Eseguito automaticamente da Prepare in Fase B. Ordine ottimizzato per ridurre overhead:
tutti i tweak UI/registro sono raggruppati **prima** di Windows Update (operazione lenta).
Ogni step ha validazione `Assert-Step`.
Eseguito automaticamente da Prepare in Fase B. Steps 1/2/3/4b/5b/5c sono
**validation-only** — verificano che Deploy abbia già applicato l'hardening OS.
Steps 4/5/6/6b/7-10 eseguono la CI customization effettiva.
Ogni step ha validazione `Assert-Step` (throw on failure).
```
Step 1 Firewall off (Domain/Private/Public) ── primo, rimuove blocchi
Step 2 Defender + Antimalware off (Set-MpPreference + GPO)
── prima di WinRM e WU
Step 3 WinRM hardening (Basic, AllowUnencrypted, 2 GB shell mem, 2h timeout)
Step 4 Build user account (ci_build, admin, no expire) ── prima di WU
Step 4b UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
Step 1 Firewall: validation only — Deploy disabled all profiles [Assert-Step]
Step 2 Defender: validation only — Deploy set DisableAntiSpyware GPO + RTP off [Assert-Step]
Step 3 WinRM: validation only — Deploy set Basic/Unencrypted/MaxMem/Timeout [Assert-Step]
Step 4 Build user account (ci_build, admin, no expire) ── crea utente CI
Step 4b UAC: validation only — Deploy set EnableLUA=0 + TokenFilterPolicy [Assert-Step]
Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU
Step 5b Explorer LaunchTo=1 (This PC)
Step 5c CI UX hardening: Server Manager off, DisableCAD, OOBE/telemetry off
Step 6 Windows Update via Scheduled Task SYSTEM ── senza scan Defender, C:\CI presente
Step 5b Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive [Assert-Step]
Step 5c CI UX hardening: validation only — Deploy set lock screen, SM policy/task,
DisableCAD, OOBE, telemetry [Assert-Step]
Step 6 Windows Update via Scheduled Task SYSTEM ── clears Deploy GPO locks, runs WU
Step 6b Windows Update permanently disabled (post-update lockdown)
Step 7 .NET SDK install (channel via dotnet-install.ps1)
Step 8 Python install (3.13.3, all-users, C:\Python)
@@ -168,41 +174,36 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
### Razionale dell'ordine
- **Firewall first** — Windows blocca alcune chiamate WinRM finché non è classificato il
profilo di rete. Off subito → niente race condition durante Step 3.
- **Defender second** — disabilitato prima di WinRM e WU. WU scarica/installa update
senza scan (5-15 min risparmiati). Step 7-9 (download + install ~200-400 MB di
payload .NET/Python/VS) non vengono scansionati.
- **WinRM third** — già non interferito da firewall/defender. Hardening pulito.
- **User + UAC (4/4b)** — operazioni registro rapide; UAC off prima di dir/tool evita
prompt di elevazione; nessun motivo di aspettare WU.
- **Steps 1-3 (validazioni)** — verificano prima le precondizioni Deploy (firewall off,
Defender off, WinRM pronto) in modo che i failure emergano subito, non a metà toolchain.
- **User + UAC (4/4b)** — crea `ci_build`; valida UAC off (set da Deploy); UAC off prima
di dir/tool evita prompt di elevazione negli step 7-9.
- **CI dirs prima di WU (Step 5)** — il worker WU scrive file temp in `C:\CI\`
(`wu_worker.ps1`, `wu_result.txt`, ecc.); la directory deve esistere prima.
- **UI tweaks (5b/5c)** — tutti i tweak registro raggruppati prima del WU (lento);
scritture veloci, nessuna dipendenza da WU o toolchain.
- **WU sixth** — gira con tutto già off + `C:\CI` presente; condizioni ottimali.
- **WU disable subito dopo** — snapshot cattura WU permanentemente off.
- **UI tweaks (5b/5c)** — validazioni registro rapide; raggruppate prima di WU (lento).
- **WU (Step 6)** — clears i GPO lock di Deploy (Step A), avvia COM API via SYSTEM task,
poi Step 6b re-disabilita tutto → snapshot cattura WU permanentemente off.
- **Toolchain (7-9) last** — WU completato, nessun scan su payload installer.
### Validazioni per step (sintesi)
| Step | Check chiave |
| ---- | ------------------------------------------------------------------------------- |
| 1 | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` |
| 2 | 6 GPO key + `Set-MpPreference DisableRealtimeMonitoring=true` + ExclusionPath |
| 3 | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 |
| 4 | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Ogni dir Test-Path -PathType Container |
| 5b | HKCU LaunchTo=1 |
| 5c | ServerManager machine policy DoNotOpenAtLogon=1, HKCU DoNotOpenServerManagerAtLogon=1, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0 |
| 6 | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
| 6b | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
| 7 | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` |
| 8 | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` |
| 9 | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente |
| 10 | dotnet+python+msbuild tutti risolvibili (throw, non più warn) |
| Final| WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` |
| Step | Natura | Check chiave |
| ---- | ---------- | ------------------------------------------------------------------------------ |
| 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 |
| 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Operativo | Ogni dir Test-Path -PathType Container |
| 5b | Assert | HKCU LaunchTo=1 |
| 5c | Assert | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0 |
| 6 | Operativo | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
| 6b | Operativo | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
| 7 | Operativo | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` |
| 8 | Operativo | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` |
| 9 | Operativo | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente |
| 10 | Operativo | dotnet+python+msbuild tutti risolvibili (throw) |
| Final| Cross-cut | WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` |
---
+43 -13
View File
@@ -36,13 +36,19 @@
Hardening done by post-install.ps1 inside the guest:
- UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
- Defender real-time protection off
- Defender real-time protection off + DisableAntiSpyware=1 GPO
- DiagTrack/Telemetry off
- Windows Update services disabled
- RDP enabled (no NLA, lab only) + firewall rule
- WinRM enabled HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth,
AllowUnencrypted, TrustedHosts=*
- Windows Update GPO locks (NoAutoUpdate=1, DisableWindowsUpdateAccess=1);
services remain Manual — Setup-WinBuild2025 Step 6b permanently disables
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
- ICMPv4 inbound allowed
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
Setup-WinBuild2025 Step 5b/5c validate all of these
- VMware Tools installed (Complete) from Tools ISO
- install_complete.flag dropped in C:\Windows\Temp
@@ -506,15 +512,22 @@ sc.exe config DiagTrack start= disabled | Out-Null
sc.exe stop DiagTrack | Out-Null
L 'Telemetry disabled'
# Windows Update services disabled
foreach (`$svc in 'wuauserv','UsoSvc','WaaSMedicSvc') {
sc.exe config `$svc start= disabled | Out-Null
sc.exe stop `$svc | Out-Null
}
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
L 'Windows Update disabled'
# Windows Update GPO locks services left Manual (§7.2 Strategy B)
# Services remain at Windows default (Manual/on-demand). Setup-WinBuild2025
# Step 6 invokes the WU COM API via a SYSTEM scheduled task; it clears these
# GPO keys first (Step A), applies updates, then permanently disables services
# in Step 6b (post-update lockdown). These keys block background auto-start
# between Deploy and Setup without breaking the on-demand COM API.
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' /v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f | Out-Null
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
L 'Windows Update GPO locks set (services remain Manual; Setup Step 6b permanently disables)'
# Firewall: profiles stay default; allow RDP + ICMP + WinRM HTTPS
# Firewall: all profiles disabled + allow RDP + ICMP + WinRM HTTPS
# Disabling all profiles removes any block on subsequent WinRM config and WU
# downloads. This VM lives on VMnet8 NAT behind the VMware NAT gateway no
# inbound exposure from outside the host. Setup-WinBuild2025 Step 1 validates.
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False
L 'Windows Firewall disabled on all profiles'
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' /v fDenyTSConnections /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' /v UserAuthentication /t REG_DWORD /d 0 /f | Out-Null
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue
@@ -533,6 +546,11 @@ try {
New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null
L 'WinRM HTTPS listener configured'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>`$null
Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>`$null
Set-Service -Name WinRM -StartupType Automatic 3>`$null
L 'WinRM shell limits: MaxMemory=2048MB IdleTimeout=2h MaxProcesses=25; StartType=Automatic'
# Server Manager / WAC popup / first-logon UX
# Server Manager auto-start off machine-wide + current Administrator + Default
@@ -549,6 +567,18 @@ reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v Disa
# OOBE privacy experience suppressed
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Server Manager + WAC + CAD + OOBE prompts disabled'
# Supplemental UX hardening validated by Setup-WinBuild2025 Step 5c:
# Lock screen: suppress at CI console sessions (no interactive user ever logs in)
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Personalization' /v NoLockScreen /t REG_DWORD /d 1 /f | Out-Null
# Server Manager GPO policy key (authoritative path on WS2025; HKLM key above is not always honoured)
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' /v DoNotOpenAtLogon /t REG_DWORD /d 1 /f | Out-Null
# Server Manager scheduled task the actual launcher on WS2025; registry alone insufficient
schtasks /Change /TN '\Microsoft\Windows\Server Manager\ServerManager' /Disable 2>&1 | Out-Null
# DisableCAD in Winlogon (belt-and-suspenders with Policies\System key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
# OOBE non-policy key (supplements the Policies\Windows\OOBE key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Lock screen, SM GPO policy + task, CAD Winlogon, OOBE non-policy key set'
# IE Enhanced Security Configuration off (Admin)
# Long-known CI papercut: ESC blocks every URL in admin sessions.
+66 -170
View File
@@ -8,40 +8,40 @@
This script is run ONE TIME inside the template VM before taking the
"BaseClean" snapshot. After the snapshot is taken, never modify the VM.
Installs and configures (each step followed by Assert-Step validation):
Step 1 — Firewall: all profiles disabled first — removes any block on subsequent steps
Prerequisite: Deploy-WinBuild2025.ps1 must have completed first (unattended OS
install + post-install.ps1 hardening). Steps 1, 3, 4b, 5b, 5c are validation-only
— they assert that Deploy already set the expected state rather than re-applying it.
Steps 4, 5, 6/6b, 7-10 perform actual CI customisation.
Steps and Assert-Step validation:
Step 1 — Firewall: validation only — Deploy disabled all profiles.
Validation: Domain/Private/Public all Enabled=False
Step 2 — Defender state validation only (disabled by Deploy-WinBuild2025
during unattended install via DisableAntiSpyware GPO + RTP off).
Validation: GPO DisableAntiSpyware=1. No exclusion paths — with
Defender off the scanner isn't running, so exclusions are moot.
Step 3 — WinRM: Basic auth, AllowUnencrypted, MaxMemoryPerShellMB=2048, 2h timeout
Firewall + Defender already off — no interference during config
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 4 — Local build user account ($BuildUsername, PasswordNeverExpires, Administrators)
Validation: user exists, enabled, PasswordNeverExpires, admin membership
Step 4b — UAC disabled (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
Step 4b — UAC: validation only — Deploy set EnableLUA=0, LocalAccountTokenFilterPolicy=1.
Validation: both registry values verified
Step 5 — CI working directories: C:\CI\build, C:\CI\output, C:\CI\scripts
Must run before Windows Update — WU worker writes temp files to C:\CI
Validation: each path exists as Container
Step 5b — Explorer LaunchTo=1 (This PC) for current user + Default profile hive
Step 5b — Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive.
Validation: HKCU LaunchTo=1
Step 5c — CI UX hardening (all UI tweaks before the long-running WU step):
Server Manager auto-start off (machine policy + HKCU Administrator +
Default User hive → ci_build inherits at first logon)
Ctrl+Alt+Del at login disabled (DisableCAD=1) — CI VMs never need it
OOBE privacy/telemetry prompt suppressed (DisablePrivacyExperience=1,
AllowTelemetry=0) — prevents first-logon interactive prompt in clones
Validation: machine policy key, HKCU key, DisableCAD, OOBE policy, telemetry
Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit)
Runs with Firewall + Defender off, C:\CI present — faster, no scan overhead
Step 5c — CI UX hardening: validation only — Deploy set lock screen, Server Manager
(policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System
+ Winlogon), OOBE/telemetry suppressed.
Validation: NoLockScreen, SM policy, SM task, DisableCAD, OOBE, telemetry
Step 6 — Windows Update via Scheduled Task as SYSTEM (avoids WinRM token limit).
Clears Deploy's GPO locks (Step A), starts services (Step D), runs COM API,
reports result code. Skipped with -SkipWindowsUpdate.
Validation: result code 0/2/3, no reboot required (else exit 3010)
Step 6b — Windows Update permanently disabled (post-update lockdown)
Step 6b — Windows Update permanently disabled (post-update lockdown — always runs).
wuauserv + UsoSvc set to Disabled; GPO keys NoAutoUpdate=1,
DisableWindowsUpdateAccess=1; WU scheduled tasks disabled
Runs unconditionally (even with -SkipWindowsUpdate) so snapshot
captures WU off — linked clones never trigger background updates
DisableWindowsUpdateAccess=1; WU scheduled tasks disabled.
Snapshot captures WU permanently off — linked clones never auto-update.
Validation: wuauserv StartType=Disabled, GPO keys present
Step 7 — .NET SDK (channel $DotNetSdkVersion) via dotnet-install.ps1
Validation: dotnet resolvable, version channel match, machine PATH
@@ -50,28 +50,27 @@
Step 9 — Visual Studio Build Tools 2026 via Scheduled Task as SYSTEM
Validation: MSBuild.exe exists, executes, PATH, VC dir present
Step 10 — Toolchain cross-check (dotnet, python, msbuild)
Throws on any missing tool (was: warn-only)
Throws on any missing tool
Cleanup — Disk cleanup: cleanmgr, SoftwareDistribution, CBS, TEMP, Prefetch, DISM
wuauserv already Disabled (Step 6b) — not restarted after cache clear
SoftwareDistribution\Download cleared best-effort (WaaSMedicSvc may repopulate)
Validation: wuauserv StartType Disabled (SoftwareDistribution check removed —
WaaSMedicSvc tamper-protection restores files immediately)
Validation: wuauserv StartType Disabled
Final — Pre-snapshot gate: 7 cross-cutting checks across all steps
Throws if any check fails — prevents snapshot of broken state
Step ordering rationale:
Firewall first — removes any network block before WinRM config and WU downloads
Defender second — eliminates scan overhead before WinRM, WU, and installer downloads
WinRM third configured clean with no Firewall/Defender interference
User + UAC — quick registry ops; UAC off before dirs/tools avoids elevation prompts
CI dirs — C:\CI must exist before WU worker writes temp files there
UI tweaks — all fast registry writes batched before the slow WU step
WU sixth — all prereqs (dirs, user, tweaks) done; Firewall+Defender off
WU disable — immediately after WU completes; snapshot captures WU permanently off
Toolchain — .NET / Python / VS last; WU done, no scan on installer payloads
Firewall (Step 1) — validates Deploy disabled all profiles; assertion before WinRM/WU
Defender (Step 2) — validates Deploy disabled RTP; assertion before tool install
WinRM (Step 3) validates Deploy configured WinRM; assertion before remote steps
User (Step 4) — creates ci_build; must exist before UAC and dirs assertions
UAC (Step 4b) — validates Deploy disabled UAC (elevation-free installs in 7-9)
CI dirs (Step 5) — C:\CI must exist before WU worker writes temp files there
UI tweaks (5b/5c)validates Deploy UX settings; fast assertions before slow WU
WU (Step 6) — all prereqs validated; WU runs with Firewall+Defender already off
WU disable (Step 6b) — immediately after WU; snapshot captures WU permanently off
Toolchain (7-9) — .NET/Python/VS last; WU done, no scan on installer payloads
NOTE: Git is NOT installed. The host clones the repository and copies
the source tree into the VM via WinRM (Option B isolation model).
NOTE: Git is NOT installed by default. See §6.6 (Tier-1 Toolchain) in TODO.md
for the planned addition. The host clones and copies source via WinRM until then.
╔══════════════════════════════════════════════════════════════════════╗
║ NETWORK REQUIREMENT DURING SETUP ║
@@ -173,16 +172,12 @@ foreach ($f in $knownTempFiles) {
}
}
# ── Step 1: Firewall ──────────────────────────────────────────────────────────
Write-Step "Disabling Windows Firewall (all profiles) — isolated lab VM"
# ── Step 1: Firewall (validation only — disabled by Deploy-WinBuild2025) ──────
Write-Step "Firewall state (validation onlydisabled at deploy time)"
# Disable first: removes any block on subsequent WinRM config and WU downloads.
# This VM lives on VMnet8 NAT behind the VMware NAT gateway — no inbound exposure
# from outside the host, so full disable is acceptable.
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False
Write-Host "Windows Firewall disabled on all profiles."
# Deploy-WinBuild2025.ps1 post-install.ps1 called Set-NetFirewallProfile -Enabled False
# on all profiles. Validated here before WinRM and WU assertions proceed.
# Validation
foreach ($p in 'Domain','Private','Public') {
Assert-Step 'Firewall' "$p profile disabled" {
(Get-NetFirewallProfile -Profile $p -ErrorAction Stop).Enabled -eq $false
@@ -201,29 +196,13 @@ Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
((Get-ItemProperty -Path $key -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware -eq 1)
}
# ── Step 3: WinRM ─────────────────────────────────────────────────────────────
Write-Step "Configuring WinRM"
# ── Step 3: WinRM (validation only — configured by Deploy-WinBuild2025) ───────
Write-Step "WinRM state (validation only — configured at deploy time)"
# Firewall and Defender already off — no interference during WinRM hardening.
Enable-PSRemoting -Force -SkipNetworkProfileCheck 3>$null | Out-Null
# 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.
# Allow unencrypted (HTTP/5985) — acceptable for an isolated lab network.
# Migrate to HTTPS/5986 with a self-signed cert for hardened environments.
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
# Increase shell memory and timeout limits for long builds
winrm set winrm/config/winrs '@{MaxProcessesPerShell="25"}' | Out-Null
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force 3>$null
Set-Item WSMan:\localhost\Shell\IdleTimeOut 7200000 -Force 3>$null # 2 hours
# Ensure WinRM starts automatically
Set-Service -Name WinRM -StartupType Automatic 3>$null
Start-Service WinRM 3>$null
Write-Host "WinRM configured."
# Validation
Assert-Step 'WinRM' 'service Running' {
(Get-Service WinRM -ErrorAction Stop).Status -eq 'Running'
}
@@ -294,19 +273,13 @@ Assert-Step 'User' "user '$BuildUsername' member of Administrators" {
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
}
# ── Step 4b: Disable UAC ─────────────────────────────────────────────────────
Write-Step "Disabling UAC (isolated lab VM)"
# ── Step 4b: UAC (validation only — disabled by Deploy-WinBuild2025) ─────────
Write-Step "UAC state (validation only — disabled at deploy time)"
# EnableLUA=0 disables UAC entirely — admin processes get full token without prompt.
# LocalAccountTokenFilterPolicy=1 ensures remote admin connections (WinRM) also
# get an elevated token (avoids the filtered-token issue with runProgramInGuest).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name EnableLUA -Value 0 -Type DWord -Force
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name LocalAccountTokenFilterPolicy -Value 1 -Type DWord -Force
Write-Host "UAC disabled."
# Deploy-WinBuild2025.ps1 post-install.ps1 set EnableLUA=0 (no prompt for admin
# processes) and LocalAccountTokenFilterPolicy=1 (WinRM remote connections get full
# elevated token, required for runProgramInGuest and remote admin commands).
# Validation
$uacPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Assert-Step 'UAC' 'EnableLUA=0' {
(Get-ItemProperty -Path $uacPolicyKey -Name EnableLUA -ErrorAction Stop).EnableLUA -eq 0
@@ -335,109 +308,32 @@ foreach ($dir in $ciDirs) {
}
}
# ── Step 5b: Explorer settings ───────────────────────────────────────────────
Write-Step "Configuring Explorer defaults"
# ── Step 5b: Explorer settings (validation only — configured by Deploy-WinBuild2025) ──
Write-Step "Explorer defaults (validation only — configured at deploy time)"
# Deploy-WinBuild2025.ps1 post-install.ps1 set LaunchTo=1 for HKCU (Administrator)
# and the Default User hive (so ci_build and any new user inherits at first logon).
# Open Explorer to 'This PC' (Devices and Drives) instead of Quick Access.
# LaunchTo=1 = This PC, LaunchTo=2 = Quick Access (default)
$explorerAdvKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
Set-ItemProperty -Path $explorerAdvKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
# Also apply to the Default User profile so ci_build and any new user inherits it.
$defaultHive = 'C:\Users\Default\NTUSER.DAT'
$mountName = 'TempDefaultUser'
if (Test-Path $defaultHive) {
reg load "HKU\$mountName" $defaultHive | Out-Null
$defKey = "Registry::HKU\$mountName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
if (-not (Test-Path $defKey)) { New-Item -Path $defKey -Force | Out-Null }
Set-ItemProperty -Path $defKey -Name 'LaunchTo' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$mountName" | Out-Null
Write-Host "Explorer: 'This PC' set for current user and default profile."
}
else {
Write-Host "Explorer: 'This PC' set for current user (default hive not found)."
}
# Validation
Assert-Step 'Explorer' "LaunchTo=1 (HKCU current user)" {
(Get-ItemProperty -Path $explorerAdvKey -Name LaunchTo -ErrorAction Stop).LaunchTo -eq 1
}
# ── Step 5c: CI UX hardening ────────────────────────────────────────────────
Write-Step "CI UX hardening (lock screen off, Server Manager off, no CAD, no OOBE)"
# ── Step 5c: CI UX hardening (validation only — configured by Deploy-WinBuild2025)
Write-Step "CI UX hardening state (validation only — configured at deploy time)"
# All UI tweaks batched here — fast registry writes before the slow WU step.
# Deploy-WinBuild2025.ps1 post-install.ps1 set: lock screen (NoLockScreen=1), Server
# Manager (policy + HKLM + task + HKCU + Default hive), DisableCAD (Policies\System +
# Winlogon), OOBE DisablePrivacyExperience (policy + non-policy), AllowTelemetry=0.
# 1 — Lock screen: disable entirely (CI VMs never need a lock screen)
# NoLockScreen=1 suppresses the lock screen shown before login on console sessions.
$personPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'
if (-not (Test-Path $personPolicyKey)) { New-Item -Path $personPolicyKey -Force | Out-Null }
Set-ItemProperty -Path $personPolicyKey -Name 'NoLockScreen' -Value 1 -Type DWord -Force
Write-Host "Lock screen disabled."
# 2 — Server Manager: do not auto-open at logon
# Three layers needed on WS2025:
# a) Machine policy key (GPO path — read by ServerManager policy check)
# b) HKLM non-policy key (read directly by ServerManager.exe on WS2025)
# c) Scheduled Task '\Microsoft\Windows\Server Manager\ServerManager'
# — this is the actual launcher; registry alone is insufficient on WS2025
# d) HKCU + Default hive for per-user belt-and-suspenders
$smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
if (-not (Test-Path $smPolicyKey)) { New-Item -Path $smPolicyKey -Force | Out-Null }
Set-ItemProperty -Path $smPolicyKey -Name 'DoNotOpenAtLogon' -Value 1 -Type DWord -Force
$smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smHKLMKey)) { New-Item -Path $smHKLMKey -Force | Out-Null }
Set-ItemProperty -Path $smHKLMKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
Disable-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -ErrorAction SilentlyContinue | Out-Null
# Current session user (Administrator)
$smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
if (-not (Test-Path $smUserKey)) { New-Item -Path $smUserKey -Force | Out-Null }
Set-ItemProperty -Path $smUserKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
# Default User hive — ci_build and any future account inherits at first logon
$smDefaultHive = 'C:\Users\Default\NTUSER.DAT'
$smMountName = 'TempDefaultUserSM'
if (Test-Path $smDefaultHive) {
reg load "HKU\$smMountName" $smDefaultHive | Out-Null
$smDefKey = "Registry::HKU\$smMountName\SOFTWARE\Microsoft\ServerManager"
if (-not (Test-Path $smDefKey)) { New-Item -Path $smDefKey -Force | Out-Null }
Set-ItemProperty -Path $smDefKey -Name 'DoNotOpenServerManagerAtLogon' -Value 1 -Type DWord -Force
[gc]::Collect()
reg unload "HKU\$smMountName" | Out-Null
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU + Default hive set."
}
else {
Write-Host "Server Manager: policy + HKLM + task + Administrator HKCU set (Default hive not found)."
}
# 3 — Disable Ctrl+Alt+Del at interactive login
# Set in both locations: Winlogon (classic) + Policies\System (policy, authoritative on WS2025).
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' `
-Name 'DisableCAD' -Value 1 -Type DWord -Force
$smPolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager'
$smHKLMKey = 'HKLM:\SOFTWARE\Microsoft\ServerManager'
$smUserKey = 'HKCU:\SOFTWARE\Microsoft\ServerManager'
$systemPolicyKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Set-ItemProperty -Path $systemPolicyKey -Name 'DisableCAD' -Value 1 -Type DWord -Force
Write-Host "Ctrl+Alt+Del at login disabled (Winlogon + Policies\System)."
$oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
$dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
# 4 — Suppress OOBE privacy/telemetry prompt
$oobePolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE'
if (-not (Test-Path $oobePolicyKey)) { New-Item -Path $oobePolicyKey -Force | Out-Null }
Set-ItemProperty -Path $oobePolicyKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$oobeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE'
if (-not (Test-Path $oobeKey)) { New-Item -Path $oobeKey -Force | Out-Null }
Set-ItemProperty -Path $oobeKey -Name 'DisablePrivacyExperience' -Value 1 -Type DWord -Force
$dcKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'
if (-not (Test-Path $dcKey)) { New-Item -Path $dcKey -Force | Out-Null }
Set-ItemProperty -Path $dcKey -Name 'AllowTelemetry' -Value 0 -Type DWord -Force
Write-Host "OOBE privacy prompt and telemetry disabled."
# Validation
Assert-Step 'CIUX' 'lock screen disabled (NoLockScreen=1)' {
(Get-ItemProperty -Path $personPolicyKey -Name NoLockScreen -ErrorAction Stop).NoLockScreen -eq 1
}