Files
local-ci-cd-system/template/Validate-SetupState.ps1
T
Simone d732e182ce test(validation): Add Git + 7-Zip checks to Validate-SetupState
§6.6 Tier-1 Toolchain (Git for Windows, 7-Zip) now included in final
state validation. Checks:
- 'Git for Windows (git.exe in PATH)'
- '7-Zip (7z.exe in PATH)'

Added to both initial validation pass and re-validation pass (post-remediation).

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

382 lines
20 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).
.PARAMETER Remediate
If set, automatically applies fixes for known remediable failures
(AutoAdminLogon, wuauserv/UsoSvc StartType=Disabled) then re-runs all checks.
.EXAMPLE
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
.EXAMPLE
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129 -Remediate
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $VMIPAddress,
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword,
[string] $BuildUsername = 'ci_build',
[string] $DotNetChannel = '10.0',
[string] $PythonVersion = '3.13.3',
[switch] $Remediate
)
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
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
try {
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
$newHosts = if ($cur) { "$cur,$VMIPAddress" } else { $VMIPAddress }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force
}
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -UseSSL -Port 5986 -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=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
}
# 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)
}
# Tier-1 Toolchain (§6.6)
Chk 'Git for Windows (git.exe in PATH)' {
Get-Command git -ErrorAction SilentlyContinue
}
Chk '7-Zip (7z.exe in PATH)' {
Get-Command 7z -ErrorAction SilentlyContinue
}
# 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|wu_result|vs_buildtools|dotnet-install' }
$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
}
if (-not $Remediate) {
Write-Host "$failed / $($checks.Count) checks FAILED." -ForegroundColor Red
Write-Host "Re-run with -Remediate to auto-fix known issues." -ForegroundColor Yellow
exit 1
}
# ── Remediation pass ─────────────────────────────────────────────────────
$failedNames = @($checks | Where-Object { -not $_.Pass } | Select-Object -ExpandProperty Name)
Write-Host "Applying remediation for $($failedNames.Count) failed check(s)..." -ForegroundColor Yellow
Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
param([string[]] $FailedNames, [string] $AdminPassword)
if ($FailedNames -contains 'AutoAdminLogon=1, DefaultUserName=Administrator') {
$wl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
Set-ItemProperty $wl AutoAdminLogon '1' -Type String
Set-ItemProperty $wl DefaultUserName 'Administrator' -Type String
Set-ItemProperty $wl DefaultDomainName '.' -Type String
Set-ItemProperty $wl DefaultPassword $AdminPassword -Type String
Write-Host " [FIX] AutoAdminLogon keys written."
}
if ($FailedNames -contains 'wuauserv StartType=Disabled') {
Set-Service -Name wuauserv -StartupType Disabled -ErrorAction SilentlyContinue
Write-Host " [FIX] wuauserv set to Disabled."
}
if ($FailedNames -contains 'UsoSvc StartType=Disabled') {
Set-Service -Name UsoSvc -StartupType Disabled -ErrorAction SilentlyContinue
Write-Host " [FIX] UsoSvc set to Disabled."
}
} -ArgumentList $failedNames, $AdminPassword
# ── Re-run checks ─────────────────────────────────────────────────────────
Write-Host "`nRe-validating after remediation..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -UseSSL -Port 5986 -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 })
}
foreach ($p in Get-NetFirewallProfile) { $n=$p.Name;$e=$p.Enabled; Chk "Firewall $n disabled" { $e -eq $false } }
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=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'
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 }
Chk 'Explorer LaunchTo=1 (HKCU)' { (Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name LaunchTo -EA Stop).LaunchTo -eq 1 }
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' }
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 }
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) }
foreach ($dir in 'C:\CI\build','C:\CI\output','C:\CI\scripts') { Chk "$dir exists" { Test-Path $dir -PathType Container } }
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 -EA SilentlyContinue; $msb -and (Test-Path $msb.Source) }
Chk 'Git for Windows (git.exe in PATH)' { Get-Command git -ErrorAction SilentlyContinue }
Chk '7-Zip (7z.exe in PATH)' { Get-Command 7z -ErrorAction SilentlyContinue }
Chk 'no leftover installer scripts in C:\CI' { $l=Get-ChildItem 'C:\CI' -File -EA SilentlyContinue | Where-Object { $_.Name -match 'wu_worker|wu_result|vs_buildtools|dotnet-install' }; $l.Count -eq 0 }
return $results.ToArray()
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion
Write-Host ''
$failed = 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 still FAILED after remediation." -ForegroundColor Red
exit 1
}
} finally {
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
}