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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 11:30:11 +02:00

260 lines
11 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Validates full post-Setup state of a template VM (Deploy + Setup).
.DESCRIPTION
Connects to the VM via WinRM (HTTP/Basic) and runs all checks for the
final BaseClean snapshot state: Deploy OS hardening + Setup CI toolchain.
Run this after Prepare-WinBuild2025.ps1 completes (exit 0) to confirm
the VM is ready for snapshot and production use.
.PARAMETER VMIPAddress
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
.PARAMETER AdminUsername
Admin account inside the VM (default: Administrator).
.PARAMETER AdminPassword
Password for the admin account. Prompted if omitted.
.PARAMETER BuildUsername
CI build account created by Setup (default: ci_build).
.PARAMETER DotNetChannel
Expected .NET SDK channel prefix (default: 10.0).
.PARAMETER PythonVersion
Expected exact Python version string (default: 3.13.3).
.EXAMPLE
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $VMIPAddress,
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword,
[string] $BuildUsername = 'ci_build',
[string] $DotNetChannel = '10.0',
[string] $PythonVersion = '3.13.3'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $AdminPassword) {
$secure = Read-Host "Password for $AdminUsername@$VMIPAddress" -AsSecureString
$AdminPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
}
$cred = New-Object System.Management.Automation.PSCredential(
$AdminUsername,
(ConvertTo-SecureString $AdminPassword -AsPlainText -Force)
)
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
Set-Item WSMan:\localhost\Client\TrustedHosts -Value ($cur ? "$cur,$VMIPAddress" : $VMIPAddress) -Force
}
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -SessionOption $so -ScriptBlock {
param($BuildUsername, $DotNetChannel, $PythonVersion)
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
function Chk {
param([string]$Name, [scriptblock]$Test)
try { $ok = [bool](& $Test); $err = '' }
catch { $ok = $false; $err = $_.Exception.Message }
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
}
# ════════════════════════════════════════════════════════════════════
# DEPLOY checks (same as Validate-DeployState.ps1)
# ════════════════════════════════════════════════════════════════════
# Firewall
foreach ($p in Get-NetFirewallProfile) {
$n = $p.Name; $e = $p.Enabled
Chk "Firewall $n disabled" { $e -eq $false }
}
# Defender
Chk 'Defender GPO DisableAntiSpyware=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' `
-Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1
}
# WinRM
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
Chk 'WinRM AllowUnencrypted=true' {
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
}
Chk 'WinRM Auth/Basic=true' {
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
}
Chk 'WinRM MaxMemoryPerShellMB >= 2048' {
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048
}
# UAC
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Chk 'UAC EnableLUA=0' {
(Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0
}
Chk 'UAC LocalAccountTokenFilterPolicy=1' {
(Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1
}
# Explorer
Chk 'Explorer LaunchTo=1 (HKCU)' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
-Name LaunchTo -EA Stop).LaunchTo -eq 1
}
# UX hardening
Chk 'NoLockScreen=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' `
-Name NoLockScreen -EA Stop).NoLockScreen -eq 1
}
Chk 'Server Manager policy DoNotOpenAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' `
-Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1
}
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'Server Manager task Disabled' {
$t = Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
-TaskName 'ServerManager' -EA SilentlyContinue
(-not $t) -or ($t.State -eq 'Disabled')
}
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' {
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' `
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
}
Chk 'DisableCAD=1 (Policies\System)' {
(Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1
}
Chk 'OOBE DisablePrivacyExperience=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' `
-Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1
}
Chk 'DataCollection AllowTelemetry=0' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' `
-Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0
}
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' {
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
}
# ════════════════════════════════════════════════════════════════════
# SETUP checks (post-Prepare state)
# ════════════════════════════════════════════════════════════════════
# CI build user
Chk "user $BuildUsername exists" {
$null -ne (Get-LocalUser -Name $BuildUsername -EA Stop)
}
Chk "user $BuildUsername enabled" {
(Get-LocalUser -Name $BuildUsername -EA Stop).Enabled
}
Chk "user $BuildUsername PasswordNeverExpires" {
(Get-LocalUser -Name $BuildUsername -EA Stop).PasswordExpires -eq $null
}
Chk "user $BuildUsername member of Administrators" {
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
}
# CI directories
foreach ($dir in 'C:\CI\build','C:\CI\output','C:\CI\scripts') {
Chk "$dir exists" { Test-Path $dir -PathType Container }
}
# Windows Update permanently disabled
Chk 'wuauserv StartType=Disabled' { (Get-Service wuauserv -EA Stop).StartType -eq 'Disabled' }
Chk 'UsoSvc StartType=Disabled' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Disabled' }
Chk 'WU GPO NoAutoUpdate=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' `
-Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1
}
Chk 'WU GPO DisableWindowsUpdateAccess=1' {
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' `
-Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1
}
# Toolchain
Chk ".NET SDK channel $DotNetChannel" {
$v = (& dotnet --version 2>&1) -as [string]
$v -and $v.StartsWith($DotNetChannel)
}
Chk 'Python C:\Python\python.exe present' { Test-Path 'C:\Python\python.exe' }
Chk "Python version $PythonVersion" {
$v = (& 'C:\Python\python.exe' --version 2>&1) -as [string]
$v -and $v.Trim() -eq "Python $PythonVersion"
}
Chk 'MSBuild.exe present' {
$msb = Get-Command msbuild -ErrorAction SilentlyContinue
$msb -and (Test-Path $msb.Source)
}
# No leftover installer files in C:\CI root
Chk 'no leftover installer scripts in C:\CI' {
$leftovers = Get-ChildItem 'C:\CI' -File -EA SilentlyContinue |
Where-Object { $_.Name -match 'wu_worker|vs_buildtools|dotnet-install|Setup-WinBuild' }
$leftovers.Count -eq 0
}
return $results.ToArray()
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion
Write-Host ''
$failed = 0
$deployCnt = 0
$setupCnt = 0
$inSetup = $false
foreach ($r in $checks) {
if (-not $inSetup -and $r.Name -match "^user $BuildUsername|^C:\\CI\\|wuauserv Start|UsoSvc Start|WU GPO|\.NET SDK|Python|MSBuild|leftover") {
Write-Host "`n --- Setup checks ---" -ForegroundColor DarkCyan
$inSetup = $true
}
if ($r.Pass) {
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
} else {
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
$failed++
}
}
Write-Host ''
if ($failed -eq 0) {
Write-Host "All $($checks.Count) checks passed — VM ready for BaseClean snapshot." -ForegroundColor Green
exit 0
} else {
Write-Host "$failed / $($checks.Count) checks FAILED." -ForegroundColor Red
exit 1
}
} finally {
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
}