refactor(template): Defender validation-only + auto WU-reboot loop in Prepare
Setup-WinBuild2025.ps1 (Step 2):
- Defender is now disabled at deploy time by Deploy-WinBuild2025.ps1 via
DisableAntiSpyware=1 GPO + RTP off during unattended install.
- Step 2 reduced to a single Assert-Step sanity check (GPO key present = 1).
Exclusion paths removed: scanner is not running, they would be moot.
Prepare-WinBuild2025.ps1 (Setup invocation):
- Replace one-shot Invoke-Command + manual 're-run with -SkipWindowsUpdate'
message with an automated loop (max 10 iterations):
* Invoke-GuestSetup helper runs Setup inside the VM and returns exit code.
* Exit 3010 (ERROR_SUCCESS_REBOOT_REQUIRED) triggers Restart-Computer on
the guest, waits for WinRM via Wait-GuestWinRMReady (20 min timeout),
reopens PSSession and loops.
* Exit 0 breaks the loop; any other code throws immediately.
* Hard cap prevents infinite loops if WU never converges.
This commit is contained in:
@@ -385,43 +385,91 @@ try {
|
|||||||
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
|
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
|
||||||
|
|
||||||
# ── Run Setup-WinBuild2025.ps1 inside the VM ──────────────────────────────
|
# ── Run Setup-WinBuild2025.ps1 inside the VM ──────────────────────────────
|
||||||
Write-Host "[Prepare] Running Setup-WinBuild2025.ps1 inside VM (this will take a while)..."
|
# Setup may return exit 3010 (ERROR_SUCCESS_REBOOT_REQUIRED) when Windows
|
||||||
Write-Host "[Prepare] Output from guest:"
|
# Update applied patches that need a reboot. We loop: reboot guest, wait for
|
||||||
Write-Host "------------------------------------------------------------"
|
# WinRM, reopen session, re-invoke Setup. Steps 1-5 are idempotent (~30 s);
|
||||||
|
# Step 6 picks up where the previous run left off (no leftover updates →
|
||||||
|
# exit 0). Hard cap on iterations to avoid an infinite loop.
|
||||||
|
$maxWuIterations = 10
|
||||||
|
$rebootDeadlineMin = 20
|
||||||
|
|
||||||
$result = Invoke-Command -Session $session -ScriptBlock {
|
function Invoke-GuestSetup {
|
||||||
param($scriptPath, $scriptArgs)
|
param([Parameter(Mandatory)] $Session,
|
||||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
[Parameter(Mandatory)] [string] $ScriptPath,
|
||||||
$exitCode = 0
|
[Parameter(Mandatory)] [hashtable] $ScriptArgs)
|
||||||
try {
|
Invoke-Command -Session $Session -ScriptBlock {
|
||||||
& $scriptPath @scriptArgs
|
param($scriptPath, $scriptArgs)
|
||||||
$exitCode = $LASTEXITCODE
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
if ($null -eq $exitCode) { $exitCode = 0 }
|
$exitCode = 0
|
||||||
}
|
try {
|
||||||
catch {
|
& $scriptPath @scriptArgs
|
||||||
Write-Error $_
|
$exitCode = $LASTEXITCODE
|
||||||
$exitCode = 1
|
if ($null -eq $exitCode) { $exitCode = 0 }
|
||||||
}
|
} catch {
|
||||||
return $exitCode
|
Write-Error $_
|
||||||
} -ArgumentList $guestScriptPath, $setupArgs
|
$exitCode = 1
|
||||||
|
}
|
||||||
Write-Host "------------------------------------------------------------"
|
return $exitCode
|
||||||
|
} -ArgumentList $ScriptPath, $ScriptArgs
|
||||||
# ── Handle reboot required (exit 3010) ───────────────────────────────────
|
|
||||||
if ($result -eq 3010) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Warning "*** REBOOT REQUIRED after Windows Update. ***"
|
|
||||||
Write-Warning " The VM will reboot. Wait for it to come back up, then run:"
|
|
||||||
Write-Warning " .\Prepare-WinBuild2025.ps1 -VMIPAddress $VMIPAddress -SkipWindowsUpdate"
|
|
||||||
|
|
||||||
# Reboot the VM
|
|
||||||
Write-Host "[Prepare] Sending reboot command to VM..."
|
|
||||||
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force }
|
|
||||||
exit 3010
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result -ne 0) {
|
function Wait-GuestWinRMReady {
|
||||||
throw "Setup-WinBuild2025.ps1 exited with code $result"
|
param([Parameter(Mandatory)] [string] $IP,
|
||||||
|
[Parameter(Mandatory)] $Cred,
|
||||||
|
[Parameter(Mandatory)] $SessionOptions,
|
||||||
|
[int] $TimeoutMin = 20)
|
||||||
|
$deadline = (Get-Date).AddMinutes($TimeoutMin)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
try {
|
||||||
|
$r = Test-WSMan -ComputerName $IP -Credential $Cred `
|
||||||
|
-Authentication Basic -ErrorAction Stop
|
||||||
|
if ($r) { return $true }
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[Prepare] Running Setup-WinBuild2025.ps1 inside VM (this will take a while)..."
|
||||||
|
|
||||||
|
for ($iter = 1; $iter -le $maxWuIterations; $iter++) {
|
||||||
|
Write-Host "[Prepare] Setup iteration $iter/$maxWuIterations"
|
||||||
|
Write-Host "[Prepare] Output from guest:"
|
||||||
|
Write-Host "------------------------------------------------------------"
|
||||||
|
|
||||||
|
$result = Invoke-GuestSetup -Session $session -ScriptPath $guestScriptPath -ScriptArgs $setupArgs
|
||||||
|
|
||||||
|
Write-Host "------------------------------------------------------------"
|
||||||
|
Write-Host "[Prepare] Iteration $iter exit code: $result"
|
||||||
|
|
||||||
|
if ($result -eq 0) { break }
|
||||||
|
|
||||||
|
if ($result -ne 3010) {
|
||||||
|
throw "Setup-WinBuild2025.ps1 exited with code $result"
|
||||||
|
}
|
||||||
|
|
||||||
|
# exit 3010 → WU applied updates needing reboot. Reboot guest, wait,
|
||||||
|
# reopen session, loop. -SkipWindowsUpdate must NOT be set on retry —
|
||||||
|
# we want the next iteration to drain remaining updates.
|
||||||
|
if ($iter -eq $maxWuIterations) {
|
||||||
|
throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..."
|
||||||
|
try {
|
||||||
|
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue
|
||||||
|
} catch { }
|
||||||
|
Remove-PSSession $session -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 30 # let WinRM tear down before polling
|
||||||
|
Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..."
|
||||||
|
if (-not (Wait-GuestWinRMReady -IP $VMIPAddress -Cred $credential `
|
||||||
|
-SessionOptions $sessionOptions -TimeoutMin $rebootDeadlineMin)) {
|
||||||
|
throw "Guest did not come back online within $rebootDeadlineMin min after WU reboot"
|
||||||
|
}
|
||||||
|
Write-Host "[Prepare] Guest WinRM ready, reopening session..."
|
||||||
|
$session = New-PSSession -ComputerName $VMIPAddress -Credential $credential `
|
||||||
|
-SessionOption $sessionOptions -Authentication Basic -ErrorAction Stop
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Post-setup remote validation ──────────────────────────────────────────
|
# ── Post-setup remote validation ──────────────────────────────────────────
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
Installs and configures (each step followed by Assert-Step validation):
|
Installs and configures (each step followed by Assert-Step validation):
|
||||||
Step 1 — Firewall: all profiles disabled first — removes any block on subsequent steps
|
Step 1 — Firewall: all profiles disabled first — removes any block on subsequent steps
|
||||||
Validation: Domain/Private/Public all Enabled=False
|
Validation: Domain/Private/Public all Enabled=False
|
||||||
Step 2 — Defender & Antimalware disabled — before WinRM and Windows Update
|
Step 2 — Defender state validation only (disabled by Deploy-WinBuild2025
|
||||||
Set-MpPreference + GPO registry keys; exclusions C:\CI, C:\dotnet, C:\Python
|
during unattended install via DisableAntiSpyware GPO + RTP off).
|
||||||
Validation: 6 GPO keys + MpPreference (if WinDefend service present)
|
Validation: GPO DisableAntiSpyware=1. No exclusion paths — with
|
||||||
Rationale: WU downloads + VS/Python/dotnet installers run unscanned;
|
Defender off the scanner isn't running, so exclusions are moot.
|
||||||
saves 5-15 min; WinRM traffic not inspected
|
|
||||||
Step 3 — WinRM: Basic auth, AllowUnencrypted, MaxMemoryPerShellMB=2048, 2h timeout
|
Step 3 — WinRM: Basic auth, AllowUnencrypted, MaxMemoryPerShellMB=2048, 2h timeout
|
||||||
Firewall + Defender already off — no interference during config
|
Firewall + Defender already off — no interference during config
|
||||||
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory
|
Validation: service Running+Automatic, AllowUnencrypted, Auth/Basic, memory
|
||||||
@@ -190,67 +189,16 @@ foreach ($p in 'Domain','Private','Public') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Step 2: Disable Windows Defender & Antimalware ───────────────────────────
|
# ── Step 2: Defender — already disabled by Deploy-WinBuild2025 ───────────────
|
||||||
Write-Step "Disabling Windows Defender & Antimalware"
|
# Deploy-WinBuild2025.ps1 set RTP off + DisableAntiSpyware=1 GPO during the
|
||||||
|
# unattended install. With Defender fully off, exclusion paths are moot — the
|
||||||
# Disabled before WinRM config and Windows Update so that:
|
# scanner that would respect them isn't running. Keep a sanity check on the
|
||||||
# - WU downloads/installs are not scanned (saves 5-15 min on update-heavy runs)
|
# tamper-protect GPO key so we throw early if someone re-enabled Defender.
|
||||||
# - WinRM traffic is not intercepted by network inspection
|
Write-Step "Defender state (validation only — disabled at deploy time)"
|
||||||
# On an isolated CI template VM this trade-off is acceptable.
|
Assert-Step 'Defender' 'GPO DisableAntiSpyware=1 (set by Deploy-WinBuild2025)' {
|
||||||
$defSvc = Get-Service -Name WinDefend -ErrorAction SilentlyContinue
|
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender'
|
||||||
if ($defSvc) {
|
(Test-Path $key) -and
|
||||||
Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue 3>$null
|
((Get-ItemProperty -Path $key -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware -eq 1)
|
||||||
Set-MpPreference -DisableBehaviorMonitoring $true -ErrorAction SilentlyContinue 3>$null
|
|
||||||
Set-MpPreference -DisableIOAVProtection $true -ErrorAction SilentlyContinue 3>$null
|
|
||||||
Set-MpPreference -DisableScriptScanning $true -ErrorAction SilentlyContinue 3>$null
|
|
||||||
# Exclude CI build paths from any remaining scanning
|
|
||||||
Add-MpPreference -ExclusionPath 'C:\CI' -ErrorAction SilentlyContinue 3>$null
|
|
||||||
Add-MpPreference -ExclusionPath 'C:\dotnet' -ErrorAction SilentlyContinue 3>$null
|
|
||||||
Add-MpPreference -ExclusionPath 'C:\Python' -ErrorAction SilentlyContinue 3>$null
|
|
||||||
Write-Host "Windows Defender real-time protection disabled."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Host "Windows Defender service not found — skipping."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Disable via Group Policy registry key — survives Defender auto-updates and reboots.
|
|
||||||
# This is the same key that SCCM/GPO uses in enterprise environments.
|
|
||||||
$defenderKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender'
|
|
||||||
if (-not (Test-Path $defenderKey)) {
|
|
||||||
New-Item -Path $defenderKey -Force | Out-Null
|
|
||||||
}
|
|
||||||
Set-ItemProperty -Path $defenderKey -Name 'DisableAntiSpyware' -Value 1 -Type DWord -Force
|
|
||||||
Set-ItemProperty -Path $defenderKey -Name 'DisableAntiVirus' -Value 1 -Type DWord -Force
|
|
||||||
# Also disable via the Windows Security Center policy subkey
|
|
||||||
$rtKey = "$defenderKey\Real-Time Protection"
|
|
||||||
if (-not (Test-Path $rtKey)) {
|
|
||||||
New-Item -Path $rtKey -Force | Out-Null
|
|
||||||
}
|
|
||||||
Set-ItemProperty -Path $rtKey -Name 'DisableRealtimeMonitoring' -Value 1 -Type DWord -Force
|
|
||||||
Set-ItemProperty -Path $rtKey -Name 'DisableBehaviorMonitoring' -Value 1 -Type DWord -Force
|
|
||||||
Set-ItemProperty -Path $rtKey -Name 'DisableIOAVProtection' -Value 1 -Type DWord -Force
|
|
||||||
Set-ItemProperty -Path $rtKey -Name 'DisableScriptScanning' -Value 1 -Type DWord -Force
|
|
||||||
Write-Host "Defender GPO registry keys written (persistent across updates)."
|
|
||||||
|
|
||||||
# Validation — GPO registry keys (always applicable, survives Defender presence)
|
|
||||||
Assert-Step 'Defender' "GPO DisableAntiSpyware=1" {
|
|
||||||
(Get-ItemProperty -Path $defenderKey -Name DisableAntiSpyware -ErrorAction Stop).DisableAntiSpyware -eq 1
|
|
||||||
}
|
|
||||||
Assert-Step 'Defender' "GPO DisableAntiVirus=1" {
|
|
||||||
(Get-ItemProperty -Path $defenderKey -Name DisableAntiVirus -ErrorAction Stop).DisableAntiVirus -eq 1
|
|
||||||
}
|
|
||||||
foreach ($rtName in 'DisableRealtimeMonitoring','DisableBehaviorMonitoring','DisableIOAVProtection','DisableScriptScanning') {
|
|
||||||
Assert-Step 'Defender' "GPO Real-Time Protection $rtName=1" {
|
|
||||||
(Get-ItemProperty -Path $rtKey -Name $rtName -ErrorAction Stop).$rtName -eq 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Validation — Defender preferences (only if service exists)
|
|
||||||
if ($defSvc) {
|
|
||||||
$mp = Get-MpPreference -ErrorAction SilentlyContinue
|
|
||||||
if ($mp) {
|
|
||||||
Assert-Step 'Defender' 'Set-MpPreference DisableRealtimeMonitoring=true' { $mp.DisableRealtimeMonitoring }
|
|
||||||
Assert-Step 'Defender' "ExclusionPath contains 'C:\CI'" { $mp.ExclusionPath -contains 'C:\CI' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Step 3: WinRM ─────────────────────────────────────────────────────────────
|
# ── Step 3: WinRM ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user