Files

282 lines
12 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Validates that a guest VM has been correctly configured.
.DESCRIPTION
For Windows guests: connects via WinRM (HTTPS/Basic) and runs all checks
that Install-CIToolchain-WinBuild2025.ps1 asserts as validation-only.
For Linux guests: connects via SSH and verifies that ci-report-ip.service
is enabled and that the ci-report-ip.sh script is executable.
Use this after Deploy completes and before taking the BaseClean snapshot.
.PARAMETER VMIPAddress
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
.PARAMETER GuestOS
Target OS: Windows (default) or Linux.
.PARAMETER AdminUsername
Windows only — admin account inside the VM (default: Administrator).
Prompted if -GuestOS Windows and -AdminPassword is not provided.
.PARAMETER AdminPassword
Windows only — password for the admin account.
.PARAMETER SshKeyPath
Linux only — path to the SSH private key (no passphrase).
Default: F:\CI\keys\ci_linux
.PARAMETER SshUser
Linux only — SSH login user inside the VM.
Default: ci_build
.EXAMPLE
# Windows
.\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
.EXAMPLE
# Linux
.\Validate-DeployState.ps1 -VMIPAddress 192.168.79.200 -GuestOS Linux
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
Justification = 'Template validation script: plain-text password converted to SecureString for WinRM PSCredential. Intentional by design.')]
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $VMIPAddress,
[ValidateSet('Windows', 'Linux')]
[string] $GuestOS = 'Windows',
# Windows params
[string] $AdminUsername = 'Administrator',
[string] $AdminPassword,
# Linux params
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
[string] $SshUser = 'ci_build'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Linux branch ──────────────────────────────────────────────────────────────
if ($GuestOS -eq 'Linux') {
if (-not (Test-Path $SshKeyPath -PathType Leaf)) {
Write-Error "SSH key not found: $SshKeyPath"
exit 1
}
$sshBase = @(
'-i', $SshKeyPath,
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=NUL',
'-o', 'ConnectTimeout=10',
'-o', 'BatchMode=yes',
"$SshUser@$VMIPAddress"
)
Write-Host "`nValidating Linux Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$checks = [System.Collections.Generic.List[pscustomobject]]::new()
function Add-LinuxCheck {
param([string]$Name, [string]$SshCommand, [scriptblock]$Validator)
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
$out = (& ssh.exe @sshBase $SshCommand 2>&1)
$rc = $LASTEXITCODE
$ErrorActionPreference = $savedEap
$pass = [bool](& $Validator $out $rc)
$checks.Add([pscustomobject]@{ Name = $Name; Pass = $pass; Err = if (-not $pass) { "rc=$rc out=$out" } else { '' } })
}
# ci-report-ip.service must be enabled
Add-LinuxCheck `
-Name 'ci-report-ip.service enabled' `
-SshCommand 'systemctl is-enabled ci-report-ip.service 2>/dev/null' `
-Validator { param($out, $rc) $rc -eq 0 -and "$out".Trim() -eq 'enabled' }
# ci-report-ip.sh must be executable
Add-LinuxCheck `
-Name '/usr/local/bin/ci-report-ip.sh executable' `
-SshCommand 'test -x /usr/local/bin/ci-report-ip.sh && echo ok' `
-Validator { param($out, $rc) $rc -eq 0 }
Write-Host ''
$failed = 0
foreach ($r in $checks) {
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) Linux Deploy checks passed." -ForegroundColor Green
exit 0
} else {
Write-Host "$failed / $($checks.Count) Linux Deploy checks FAILED." -ForegroundColor Red
exit 1
}
}
# ── Windows branch (WinRM HTTPS) ──────────────────────────────────────────────
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 Deploy state on $VMIPAddress..." -ForegroundColor Cyan
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so -ScriptBlock {
$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 })
}
# ── 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'
}
# ── Windows Update GPO + services ─────────────────────────────────────
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 'wuauserv StartType=Manual' { (Get-Service wuauserv -EA Stop).StartType -eq 'Manual' }
Chk 'UsoSvc StartType=Manual' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Manual' }
return $results.ToArray()
}
Write-Host ''
$failed = 0
foreach ($r in $checks) {
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) Deploy checks passed." -ForegroundColor Green
exit 0
} else {
Write-Host "$failed / $($checks.Count) Deploy checks FAILED." -ForegroundColor Red
exit 1
}
} finally {
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
}