#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 }