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>
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
#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
|
||||
}
|
||||
Reference in New Issue
Block a user