diff --git a/docs/WINDOWS-TEMPLATE-SETUP.md b/docs/WINDOWS-TEMPLATE-SETUP.md index 5408d14..360d76d 100644 --- a/docs/WINDOWS-TEMPLATE-SETUP.md +++ b/docs/WINDOWS-TEMPLATE-SETUP.md @@ -198,7 +198,7 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 5 | Operativo | Ogni dir Test-Path -PathType Container | | 5b | Assert | HKCU LaunchTo=1 | -| 5c | Assert | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0, AutoAdminLogon=1 + DefaultUserName=Administrator | +| 5c | Assert+Op | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0; AutoAdminLogon=1+DefaultUserName=Administrator re-affermati se assenti (Windows OOBE può resettarli al primo boot) | | 6 | Operativo | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) | | 6b | Operativo | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 | | 7 | Operativo | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` | diff --git a/template/Deploy-WinBuild2025.ps1 b/template/Deploy-WinBuild2025.ps1 index 02e124c..46df1bc 100644 --- a/template/Deploy-WinBuild2025.ps1 +++ b/template/Deploy-WinBuild2025.ps1 @@ -1110,4 +1110,8 @@ Write-Host " RDP : mstsc /v:$guestIP" Write-Host " WinRM HTTP : 5985" Write-Host " WinRM HTTPS : 5986 (self-signed)" Write-Host "" -Write-Host " VM is powered on with vmxnet3 + DHCP. Live snapshot taken." -ForegroundColor Green +Write-Host " Next steps (from template\ dir):" +Write-Host " .\Validate-DeployState.ps1 -VMIPAddress $guestIP" +Write-Host " .\Prepare-WinBuild2025.ps1 -VMIPAddress $guestIP [-SkipWindowsUpdate]" +Write-Host "" +Write-Host " VM is powered on with vmxnet3 + DHCP. Snapshot '$SnapshotName' taken." -ForegroundColor Green diff --git a/template/Prepare-WinBuild2025.ps1 b/template/Prepare-WinBuild2025.ps1 index c96e07a..daf635a 100644 --- a/template/Prepare-WinBuild2025.ps1 +++ b/template/Prepare-WinBuild2025.ps1 @@ -380,6 +380,7 @@ try { BuildPassword = $BuildPassword BuildUsername = $BuildUsername DotNetSdkVersion = $DotNetSdkVersion + AdminPassword = $AdminPassword } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true } diff --git a/template/Setup-WinBuild2025.ps1 b/template/Setup-WinBuild2025.ps1 index ad51b39..80e5ea3 100644 --- a/template/Setup-WinBuild2025.ps1 +++ b/template/Setup-WinBuild2025.ps1 @@ -131,7 +131,11 @@ param( [switch] $SkipWindowsUpdate, # Skip disk cleanup step (cleanmgr + DISM + cache clear) — speeds up re-runs during dev/debug - [switch] $SkipCleanup + [switch] $SkipCleanup, + + # Administrator password — used only to write DefaultPassword for autologin. + # Optional: if empty, DefaultPassword registry value is left as-is (Deploy may have set it). + [string] $AdminPassword = '' ) Set-StrictMode -Version Latest @@ -360,8 +364,23 @@ Assert-Step 'CIUX' 'OOBE policy DisablePrivacyExperience=1' { Assert-Step 'CIUX' 'DataCollection AllowTelemetry=0' { (Get-ItemProperty -Path $dcKey -Name AllowTelemetry -ErrorAction Stop).AllowTelemetry -eq 0 } +# AutoAdminLogon: operativo — Deploy sets this, but Windows Server 2025 OOBE/first-boot +# tasks can reset AutoAdminLogon to 0 after the initial login. Re-affirm without touching +# DefaultPassword (already written by Deploy's post-install.ps1 and still present). +$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' +$wlNow = Get-ItemProperty -Path $wlKey -ErrorAction SilentlyContinue +if ($wlNow.AutoAdminLogon -ne '1' -or $wlNow.DefaultUserName -ne 'Administrator') { + Set-ItemProperty -Path $wlKey -Name AutoAdminLogon -Value '1' -Type String -Force + Set-ItemProperty -Path $wlKey -Name DefaultUserName -Value 'Administrator' -Type String -Force + Set-ItemProperty -Path $wlKey -Name DefaultDomainName -Value '.' -Type String -Force +} +# DefaultPassword written unconditionally when available — previous run may have set +# AutoAdminLogon=1 without knowing the password (AdminPassword was added later). +if ($AdminPassword -ne '') { + Set-ItemProperty -Path $wlKey -Name DefaultPassword -Value $AdminPassword -Type String -Force +} Assert-Step 'CIUX' 'autologin Administrator enabled (AutoAdminLogon=1)' { - $wl = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop + $wl = Get-ItemProperty -Path $wlKey -ErrorAction Stop $wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator' } diff --git a/template/Validate-DeployState.ps1 b/template/Validate-DeployState.ps1 index dc718c0..8f835fc 100644 --- a/template/Validate-DeployState.ps1 +++ b/template/Validate-DeployState.ps1 @@ -49,7 +49,8 @@ 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 + $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 diff --git a/template/Validate-SetupState.ps1 b/template/Validate-SetupState.ps1 index b62b30b..e097d73 100644 --- a/template/Validate-SetupState.ps1 +++ b/template/Validate-SetupState.ps1 @@ -27,8 +27,15 @@ .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( @@ -37,7 +44,8 @@ param( [string] $AdminPassword, [string] $BuildUsername = 'ci_build', [string] $DotNetChannel = '10.0', - [string] $PythonVersion = '3.13.3' + [string] $PythonVersion = '3.13.3', + [switch] $Remediate ) Set-StrictMode -Version Latest @@ -62,7 +70,8 @@ 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 + $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 @@ -216,7 +225,7 @@ try { # 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' } + Where-Object { $_.Name -match 'wu_worker|wu_result|vs_buildtools|dotnet-install' } $leftovers.Count -eq 0 } @@ -248,8 +257,114 @@ try { if ($failed -eq 0) { Write-Host "All $($checks.Count) checks passed — VM ready for BaseClean snapshot." -ForegroundColor Green exit 0 - } else { + } + + 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 -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 -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=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 } + $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 '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 }