#Requires -Version 5.1 <# .SYNOPSIS Detekce a interaktivní remediace Secure Boot certifikátů (KB5062710 / KB5068202). .DESCRIPTION Script zkontroluje přítomnost nových 2023 Secure Boot certifikátů, zobrazí stav ve čtyřech sekcích a nabídne remediaci. Pokud jsou povinné 2023 certifikáty (Windows UEFI CA 2023 v DB + Microsoft Corporation KEK 2K CA 2023 v KEK) PŘED spuštěním remediace přítomny, script skončí s informací, že není nutná žádná akce. Výjimka: parametr -Force umožní remediaci přesto spustit. Kritéria úspěšné remediace (post-remediation — všechny 4 podmínky): 1. Windows UEFI CA 2023 v DB + Microsoft Corporation KEK 2K CA 2023 v KEK 2. AvailableUpdates = 0x0 nebo 0x4000 3. UEFICA2023Status = "Updated" 4. UEFICA2023Error = 0 nebo neexistuje Průchod AvailableUpdates: 0x5944 -> 0x5904 -> 0x5104 -> 0x4104 -> 0x4100 -> 0x4000 -> 0x0 Hodnota 0x5944 se nastavuje pouze jednou (při prvním spuštění nebo po dokončení); systém poté bity sám odečítá. .PARAMETER CheckOnly Pouze zobrazí stav bez dotazů a změn. Exit kódy: 0 = hotovo, 1 = nutná akce, 2 = blokováno. .PARAMETER AssumeYes Přeskočí interaktivní dotaz a remediaci rovnou aplikuje (pokud je smysluplná). .PARAMETER Force Spustí remediaci i v případě, že povinné certifikáty jsou již přítomny. .PARAMETER AutoRestart Po staged Boot Manageru (AU=0x4100) restartuje server automaticky bez dotazu. POZOR: server bude restartován okamžitě bez potvrzení. .NOTES Reference: KB5062710, KB5068202, KB5066835 (min. build 10/2025), KB5085790. Vyžaduje Administrator pro zápis registry a spuštění servicing task. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [switch]$CheckOnly, [switch]$AssumeYes, [switch]$Force, [switch]$AutoRestart ) $ErrorActionPreference = 'SilentlyContinue' Set-StrictMode -Off # Vynutit UTF-8 výstup (diakritika i ve Windows PowerShell 5.1) try { $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [Console]::OutputEncoding = $utf8NoBom $OutputEncoding = $utf8NoBom } catch { } #region ── Konstanty ────────────────────────────────────────────────────────── $REG_SECUREBOOT = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot' $REG_SERVICING = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' $AVAILABLE_UPDATES_VALUE = 0x5944 $TASK_PATH = '\Microsoft\Windows\PI\' $TASK_NAME = 'Secure-Boot-Update' $TASK_TIMEOUT_SEC = 180 $WORK_ROOT = Join-Path $env:ProgramData 'SecureBootCA2023' $STATE_FILE = Join-Path $WORK_ROOT 'state.json' $CN_KEK2023 = 'KEK 2K CA 2023' $CN_WINUEFI2023 = 'Windows UEFI CA 2023' $CN_UEFI2023 = 'Microsoft UEFI CA 2023' $CN_OPTROM2023 = 'Option ROM UEFI CA 2023' $CN_PCA2011 = 'Windows Production PCA 2011' $SYM_DONE = '+' $SYM_FAIL = '!' $SYM_PENDING = ' ' # Popisy EventID pro sekci UDÁLOSTI $EVENT_DESC = @{ 1795 = 'Chyba firmwaru — aktualizace selhala' 1796 = 'Selhání zápisu UEFI proměnné' 1799 = 'Informační: aktualizace dokončena' 1800 = 'Aktualizace zahájena' 1801 = 'Čeká na podmínky (monitor)' 1802 = 'Pozastaveno — problém firmwaru' 1803 = 'Odloženo (monitorovací podmínka)' 1808 = 'Certifikáty úspěšně aplikovány' } #endregion #region ── Log / barevný výstup ─────────────────────────────────────────────── $script:LogFile = Join-Path $WORK_ROOT 'SecureBootRemediation.log' $script:LogBuffer = New-Object System.Collections.Generic.List[string] function Add-LogLine { param([string]$Text) [void]$script:LogBuffer.Add(('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Text)) } function Flush-Log { if ($script:LogBuffer.Count -eq 0) { return } try { if (-not (Test-Path $WORK_ROOT)) { New-Item -Path $WORK_ROOT -ItemType Directory -Force | Out-Null } [System.IO.File]::AppendAllText( $script:LogFile, (($script:LogBuffer -join "`r`n") + "`r`n"), (New-Object System.Text.UTF8Encoding($false)) ) $script:LogBuffer.Clear() } catch { } } function Write-Line { param([string]$Text = '', [string]$Color = 'Gray', [switch]$NoLog) Write-Host $Text -ForegroundColor $Color if (-not $NoLog) { Add-LogLine $Text } } function Write-Rule { param([string]$Color = 'DarkCyan') Write-Host ('=' * 64) -ForegroundColor $Color } function Write-Head { param([string]$Text) Write-Host '' Write-Host $Text -ForegroundColor Cyan Add-LogLine "== $Text ==" } function Write-KV { param( [string]$Key, [string]$Value, [string]$ValueColor = 'White', [string]$Note, [string]$NoteColor = 'DarkGray' ) Write-Host (" {0,-28}: " -f $Key) -ForegroundColor Gray -NoNewline Write-Host $Value -ForegroundColor $ValueColor -NoNewline if ($Note) { Write-Host (" $Note") -ForegroundColor $NoteColor } else { Write-Host '' } Add-LogLine ("{0}: {1} {2}" -f $Key, $Value, $Note) } function Write-Check { param( [ValidateSet('Done','Pending','Fail','Info')][string]$State, [string]$Text, [string]$Note, [string]$NoteColor = 'Yellow' ) switch ($State) { 'Done' { $mark = "[$SYM_DONE]"; $mc = 'Green'; $tc = 'White' } 'Fail' { $mark = "[$SYM_FAIL]"; $mc = 'Red'; $tc = 'Red' } 'Pending' { $mark = "[ ]"; $mc = 'DarkGray'; $tc = 'Gray' } 'Info' { $mark = "[ ]"; $mc = 'DarkGray'; $tc = 'DarkGray' } } Write-Host (" {0} " -f $mark) -ForegroundColor $mc -NoNewline Write-Host $Text -ForegroundColor $tc -NoNewline if ($Note) { Write-Host (" $Note") -ForegroundColor $NoteColor } else { Write-Host '' } Add-LogLine (" [{0}] {1} {2}" -f $State, $Text, $Note) } #endregion #region ── Detekce — prostředí / HW / Secure Boot ──────────────────────────── function Get-EnvironmentType { try { $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop $mfr = [string]$cs.Manufacturer $mdl = [string]$cs.Model if ($mfr -like '*VMware*') { return 'VMware VM' } if ($mfr -like '*Microsoft*' -and $mdl -eq 'Virtual Machine') { return 'Hyper-V VM' } if ($mdl -like '*Virtual*' -or $mfr -like '*QEMU*' -or $mfr -like '*Xen*') { return 'Other VM' } return 'Physical' } catch { return 'Unknown' } } function Get-HardwareInfo { $hw = [ordered]@{ Manufacturer = 'Unknown' Model = 'Unknown' FirmwareType = 'Unknown' BiosVersion = 'Unknown' BiosReleaseDate = 'Unknown' } try { $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop $hw.Manufacturer = [string]$cs.Manufacturer $hw.Model = [string]$cs.Model } catch { } try { $bios = Get-CimInstance Win32_BIOS -ErrorAction Stop $hw.BiosVersion = [string]$bios.SMBIOSBIOSVersion if ($bios.ReleaseDate) { $hw.BiosReleaseDate = ([datetime]$bios.ReleaseDate).ToString('yyyy-MM-dd') } } catch { } $fw = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue if ($fw) { $hw.FirmwareType = if ($fw.PEFirmwareType -eq 2) { 'UEFI' } else { 'Legacy BIOS' } } elseif (Test-Path $REG_SECUREBOOT) { $hw.FirmwareType = 'UEFI' } else { $hw.FirmwareType = 'Legacy BIOS' } return $hw } function Get-SecureBootState { $s = [ordered]@{ IsUEFI = $false; IsSupported = $false; IsEnabled = $false; Error = $null } $fw = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue $s.IsUEFI = ($fw -and $fw.PEFirmwareType -eq 2) -or (Test-Path $REG_SECUREBOOT) if (-not $s.IsUEFI) { return $s } try { $r = Confirm-SecureBootUEFI -ErrorAction Stop $s.IsSupported = $true $s.IsEnabled = [bool]$r } catch { $m = $_.Exception.Message if ($m -like '*not supported*' -or $m -like '*Cmdlet not supported*') { $s.IsSupported = $false } elseif ($m -like '*disabled*') { $s.IsSupported = $true; $s.IsEnabled = $false } else { $s.IsSupported = $true; $s.IsEnabled = $false; $s.Error = $m } } return $s } function Get-SecureBootMode { $setup = $null; $audit = $null; $deployed = $null try { $setup = (Get-SecureBootUEFI -Name SetupMode -ErrorAction Stop).Bytes[0] } catch { } try { $audit = (Get-SecureBootUEFI -Name AuditMode -ErrorAction Stop).Bytes[0] } catch { } try { $deployed = (Get-SecureBootUEFI -Name DeployedMode -ErrorAction Stop).Bytes[0] } catch { } if ($setup -eq 1) { return 'Setup' } if ($audit -eq 1) { return 'Audit' } if ($deployed -eq 1) { return 'Deployed' } if ($null -ne $setup) { return 'User' } return 'Unknown' } function Get-BitLockerInfo { $i = [ordered]@{ Status = 'Unknown'; Protectors = ''; UsesPcr = $false } try { $v = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction Stop if ($v) { $i.Status = [string]$v.ProtectionStatus $types = @($v.KeyProtector | ForEach-Object { [string]$_.KeyProtectorType }) $i.Protectors = ($types -join ', ') $i.UsesPcr = [bool](@($types | Where-Object { $_ -like 'Tpm*' }).Count) } } catch { $i.Status = 'N/A' } return $i } #endregion #region ── Detekce — certifikáty / registry / události ─────────────────────── function Parse-EFISignatureList { # Parsuje EFI_SIGNATURE_LIST a vrátí pole X509Certificate2 (pouze X.509 položky). # Formát: opakující se hlavičky SignatureList, každá obsahuje seznam SignatureEntry # s 16B GUID prefixem (SignatureOwner) + DER-encoded certifikát. param([byte[]]$Bytes) $certs = @() if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs } # EFI_CERT_X509_GUID: {a5c059a1-94e4-4aa7-87b5-ab155c2bf072} (little-endian) $x509Guid = [byte[]](0xa1,0x59,0xc0,0xa5,0xe4,0x94,0xa7,0x4a,0x87,0xb5,0xab,0x15,0x5c,0x2b,0xf0,0x72) $pos = 0 while ($pos + 28 -le $Bytes.Length) { $sigTypeGuid = $Bytes[$pos..($pos+15)] $listSize = [BitConverter]::ToUInt32($Bytes, $pos+16) $headerSize = [BitConverter]::ToUInt32($Bytes, $pos+20) $sigSize = [BitConverter]::ToUInt32($Bytes, $pos+24) if ($listSize -lt 28 -or $listSize -gt ($Bytes.Length - $pos)) { break } $isX509 = $true for ($i = 0; $i -lt 16; $i++) { if ($sigTypeGuid[$i] -ne $x509Guid[$i]) { $isX509 = $false; break } } if ($isX509 -and $sigSize -gt 16) { $entryPos = $pos + 28 + $headerSize $listEnd = $pos + $listSize while ($entryPos + $sigSize -le $listEnd) { $certOffset = $entryPos + 16 # přeskočit 16B SignatureOwner GUID $certSize = [int]$sigSize - 16 if ($certOffset + $certSize -le $Bytes.Length -and $certSize -gt 0) { try { $certBytes = [byte[]]($Bytes[$certOffset..($certOffset + $certSize - 1)]) $certs += New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(, $certBytes) } catch { } } $entryPos += $sigSize } } $pos += $listSize } return $certs } function Convert-CertToInfo { param($Cert) [ordered]@{ Subject = $Cert.Subject Thumbprint = $Cert.Thumbprint NotAfter = $Cert.NotAfter.ToString('yyyy-MM-dd') } } function Get-SbVarAscii { param([ValidateSet('PK','KEK','db','dbx')][string]$Name) try { return [System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name $Name -ErrorAction Stop).Bytes) } catch { return $null } } function Get-CertificateStatus { $st = [ordered]@{ KEK = [ordered]@{ Has2011 = $false; Has2023 = $false Certs2011 = @(); Certs2023 = @() Error = $null } DB = [ordered]@{ Has2011UEFI = $false; Has2011WindowsPCA = $false Has2023UEFI = $false; Has2023OptionROM = $false Has2023WindowsUEFI = $false Certs2011 = @(); Certs2023 = @() Error = $null } DbxRevokesPCA2011 = $false AnyExpiring2011 = $false } try { $kekBytes = (Get-SecureBootUEFI -Name KEK -ErrorAction Stop).Bytes foreach ($cert in (Parse-EFISignatureList -Bytes $kekBytes)) { $subj = $cert.Subject $info = Convert-CertToInfo $cert if ($subj -like '*KEK CA 2011*') { $st.KEK.Has2011 = $true; $st.KEK.Certs2011 += $info } if ($subj -like "*$CN_KEK2023*" -or $subj -like '*KEK*CA 2023*') { $st.KEK.Has2023 = $true; $st.KEK.Certs2023 += $info } } } catch { $st.KEK.Error = $_.Exception.Message } try { $dbBytes = (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes foreach ($cert in (Parse-EFISignatureList -Bytes $dbBytes)) { $subj = $cert.Subject $info = Convert-CertToInfo $cert if ($subj -like '*UEFI CA 2011*') { $st.DB.Has2011UEFI = $true; $st.DB.Certs2011 += $info } if ($subj -like '*Windows Production PCA 2011*' -or $subj -like '*Windows PCA 2011*') { $st.DB.Has2011WindowsPCA = $true; $st.DB.Certs2011 += $info } if ($subj -like "*$CN_UEFI2023*" -and $subj -notlike '*Option ROM*' -and $subj -notlike '*Windows UEFI*') { $st.DB.Has2023UEFI = $true; $st.DB.Certs2023 += $info } if ($subj -like "*$CN_OPTROM2023*") { $st.DB.Has2023OptionROM = $true; $st.DB.Certs2023 += $info } if ($subj -like "*$CN_WINUEFI2023*") { $st.DB.Has2023WindowsUEFI = $true; $st.DB.Certs2023 += $info } } } catch { $st.DB.Error = $_.Exception.Message } # ASCII fallback — doplní booleany pokud X.509 parse selhal $kAscii = Get-SbVarAscii KEK if ($kAscii -and $kAscii -match [regex]::Escape($CN_KEK2023)) { $st.KEK.Has2023 = $true } $dAscii = Get-SbVarAscii db if ($dAscii) { if ($dAscii -match [regex]::Escape($CN_WINUEFI2023)) { $st.DB.Has2023WindowsUEFI = $true } if ($dAscii -match [regex]::Escape($CN_OPTROM2023)) { $st.DB.Has2023OptionROM = $true } if ($dAscii -match 'Microsoft UEFI CA 2023') { $st.DB.Has2023UEFI = $true } } $xAscii = Get-SbVarAscii dbx if ($xAscii -and $xAscii -match [regex]::Escape($CN_PCA2011)) { $st.DbxRevokesPCA2011 = $true } $st.AnyExpiring2011 = $st.KEK.Has2011 -or $st.DB.Has2011UEFI -or $st.DB.Has2011WindowsPCA return $st } function Get-RegistryStatus { $reg = [ordered]@{ AvailableUpdates = $null HighConfidenceOptOut = $null MicrosoftUpdateManagedOptIn = $null ServicingKeyExists = $false UEFICA2023Status = $null UEFICA2023StatusText = 'KeyNotPresent' UEFICA2023Error = $null UEFICA2023ErrorEvent = $null WindowsUEFICA2023Capable = $null WindowsUEFICA2023CapableText = '-' ConfidenceLevel = $null } $main = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue if ($main) { $reg.AvailableUpdates = $main.AvailableUpdates $reg.HighConfidenceOptOut = $main.HighConfidenceOptOut $reg.MicrosoftUpdateManagedOptIn = $main.MicrosoftUpdateManagedOptIn } $serv = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue if ($serv) { $reg.ServicingKeyExists = $true $reg.UEFICA2023Status = $serv.UEFICA2023Status $reg.UEFICA2023Error = $serv.UEFICA2023Error $reg.UEFICA2023ErrorEvent = $serv.UEFICA2023ErrorEvent $reg.WindowsUEFICA2023Capable = $serv.WindowsUEFICA2023Capable $reg.ConfidenceLevel = $serv.ConfidenceLevel } # UEFICA2023Status je REG_SZ (NotStarted/InProgress/Updated); defensivně i číselná varianta $sv = $reg.UEFICA2023Status if ($null -eq $sv) { $reg.UEFICA2023StatusText = 'KeyNotPresent' } elseif ($sv -is [string] -and $sv -ne '') { $reg.UEFICA2023StatusText = $sv } else { $reg.UEFICA2023StatusText = switch ([int]$sv) { 0 { 'NotStarted' } 1 { 'InProgress' } 2 { 'Updated' } 3 { 'Failed' } default { "($sv)" } } } $reg.WindowsUEFICA2023CapableText = switch ($reg.WindowsUEFICA2023Capable) { 0 { '0 — Windows UEFI CA 2023 není v DB' } 1 { '1 — certifikát v DB (boot manager zatím ne)' } 2 { '2 — bootuje se z 2023 boot manageru' } $null { '-' } default { [string]$reg.WindowsUEFICA2023Capable } } return $reg } function Get-AvailableUpdatesText { param($v) if ($null -eq $v) { return '(nenastaveno)' } $iv = [int]$v $hex = '0x{0:X}' -f $iv $name = switch ($iv) { 0 { 'vše hotovo (proces dokončen)' } 0x4000 { 'vše aplikováno — task čistí guard bit' } 0x4100 { 'Boot Manager 2023 nasazen na ESP — čeká RESTART' } 0x4104 { 'Microsoft UEFI CA 2023 v DB' } 0x5104 { 'Option ROM UEFI CA 2023 v DB' } 0x5904 { 'Windows UEFI CA 2023 v DB' } 0x5944 { 'naplánována plná sada (start)' } default { 'probíhá aplikace' } } return "$hex ($name)" } function Get-EventLogStatus { $e = [ordered]@{ LastEventId = $null LastEventTime = $null RelevantEvents = @() ById = @{} Error = $null } $ids = @(1795,1796,1799,1800,1801,1802,1803,1808) try { $events = Get-WinEvent -FilterHashtable @{ LogName='System'; Id=$ids } -MaxEvents 40 -ErrorAction Stop if ($events) { $sorted = $events | Sort-Object TimeCreated -Descending foreach ($id in $ids) { $hit = $sorted | Where-Object { $_.Id -eq $id } | Select-Object -First 1 if ($hit) { $e.ById[$id] = @{ Time = $hit.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') } } } foreach ($ev in ($sorted | Select-Object -First 10)) { $e.RelevantEvents += [ordered]@{ EventId = $ev.Id TimeCreated = $ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') Level = $ev.LevelDisplayName } } $last = $sorted | Select-Object -First 1 $e.LastEventId = $last.Id $e.LastEventTime = $last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') } } catch { if ($_.CategoryInfo.Reason -ne 'NoMatchingEventsException') { $e.Error = $_.Exception.Message } } return $e } function Get-TaskExists { try { $null = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop return $true } catch { return $false } } #endregion #region ── Kategorizace ─────────────────────────────────────────────────────── function Test-RemediationComplete { # Ověří všechny 4 post-remediation podmínky úspěchu. param($R) $cert = $R.Certificates $reg = $R.Registry $certsOk = $cert.KEK.Has2023 -and $cert.DB.Has2023WindowsUEFI $auVal = [int]$reg.AvailableUpdates $auOk = ($null -ne $reg.AvailableUpdates) -and ($auVal -eq 0 -or $auVal -eq 0x4000) $statusOk = $reg.UEFICA2023StatusText -eq 'Updated' $errorOk = ($null -eq $reg.UEFICA2023Error) -or ([int]$reg.UEFICA2023Error -eq 0) return $certsOk -and $auOk -and $statusOk -and $errorOk } function Get-RemediationCategory { param($Result) $sb = $Result.SecureBoot $reg = $Result.Registry $evt = $Result.EventLog $cert = $Result.Certificates $env = $Result.EnvironmentType $mode = $Result.OperatingMode # ── Základní blokátory ──────────────────────────────────────────────────── if (-not $sb.IsUEFI -or -not $sb.IsSupported) { if ($env -like '*VM*') { return @{ Code='NO_SECUREBOOT_VM'; Color='DarkGray' Label='Secure Boot nepodporováno — VM bez vTPM nebo UEFI' } } return @{ Code='NO_SECUREBOOT'; Color='DarkGray' Label='Secure Boot nepodporováno — Legacy BIOS' } } if (-not $sb.IsEnabled) { return @{ Code='SECUREBOOT_DISABLED'; Color='Yellow' Label='Secure Boot je vypnutý' } } if ($mode -eq 'Setup') { return @{ Code='SETUP_MODE'; Color='Magenta' Label='Secure Boot v Setup Mode — aktualizaci nelze dokončit (chybí enrolled PK)' } } if (-not $Result.TaskExists) { return @{ Code='TASK_MISSING'; Color='Magenta' Label='Chybí servicing task — nainstalujte kumulativní update min. z 10/2025 (KB5066835)' } } # ── Chyby a blokující stavy ─────────────────────────────────────────────── $hasErrorEvent = $reg.UEFICA2023ErrorEvent -and ([int]$reg.UEFICA2023ErrorEvent -ne 0) if ($evt.ById[1795] -or $hasErrorEvent -or $reg.UEFICA2023StatusText -eq 'Failed') { return @{ Code='UPDATE_FAILED'; Color='Red' Label='Selhání aktualizace — chyba firmwaru nebo UEFICA2023ErrorEvent' } } if ($reg.ConfidenceLevel -like 'Not Supported*') { return @{ Code='NOT_SUPPORTED'; Color='Red' Label='Zařízení nepodporuje automatickou aktualizaci (ConfidenceLevel: Not Supported)' } } if ($reg.ConfidenceLevel -like 'Temporarily Paused*' -or $evt.ById[1802]) { return @{ Code='FIRMWARE_UPDATE_NEEDED'; Color='Magenta' Label='Aktualizace pozastavena — problém firmwaru, zkontrolujte update u OEM' } } # ── Hotovo (všechny 4 post-remediation podmínky) ────────────────────────── if (Test-RemediationComplete -R $Result) { if ($cert.AnyExpiring2011) { return @{ Code='OK_TRANSITION'; Color='Green' Label='HOTOVO — 2023 certifikáty nasazeny (staré 2011 ještě přítomné, je to normální)' } } return @{ Code='OK'; Color='Green' Label='HOTOVO — 2023 certifikáty nasazeny, servisování dokončeno' } } # ── Boot Manager staged, čeká na restart ───────────────────────────────── if ([int]$reg.AvailableUpdates -eq 0x4100) { return @{ Code='UPDATE_PENDING_RESTART'; Color='Yellow' Label='Boot Manager 2023 nasazen (staged) — čeká na RESTART' } } $certsRequiredOk = $cert.KEK.Has2023 -and $cert.DB.Has2023WindowsUEFI $auNow = [int]$reg.AvailableUpdates $errOk = ($null -eq $reg.UEFICA2023Error) -or ([int]$reg.UEFICA2023Error -eq 0) # ── Certy přítomny + AU=0x0 → hotovo ───────────────────────────────────── # UEFICA2023Status může být NotStarted u strojů kde certy byly nainstalovány # starší cestou (před zavedením nové servicing infrastruktury). Pokud jsou # požadované certifikáty v DB/KEK a AvailableUpdates=0x0, považujeme za hotovo. if ($certsRequiredOk -and $auNow -eq 0 -and $errOk) { if ($cert.AnyExpiring2011) { return @{ Code='OK_TRANSITION'; Color='Green' Label='HOTOVO — 2023 certifikáty nasazeny (staré 2011 ještě přítomné, je to normální)' } } return @{ Code='OK'; Color='Green' Label='HOTOVO — 2023 certifikáty nasazeny, AvailableUpdates=0x0' } } # ── Starý build: certy přítomny, ale servicing infrastruktura chybí ────── $certsPresent = $cert.KEK.Has2023 -or $cert.DB.Has2023WindowsUEFI $bmStagedAU = ($auNow -eq 0x4100 -or $auNow -eq 0x4000) if ($certsPresent -and -not $bmStagedAU ` -and ($null -eq $reg.WindowsUEFICA2023Capable) ` -and ($null -eq $reg.UEFICA2023Status)) { return @{ Code='BUILD_OUTDATED'; Color='Magenta' Label='Build příliš starý — chybí servicing pro Boot Manager 2023, nutný Windows Update' } } # ── Částečný postup / nutná aktualizace ────────────────────────────────── if ($certsPresent) { return @{ Code='UPDATE_PARTIAL'; Color='Cyan' Label='Aktualizace probíhá — certifikáty nasazeny, zbývá dokončit servisování' } } return @{ Code='UPDATE_NEEDED'; Color='Yellow' Label='Povinné certifikáty 2023 nejsou přítomny — nutná aktualizace' } } #endregion #region ── Detekce — orchestrace ────────────────────────────────────────────── function Invoke-Detection { $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $osBuildFull = try { $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop '{0}.{1}' -f $cv.CurrentBuildNumber, $cv.UBR } catch { if ($os) { [string]$os.BuildNumber } else { '?' } } $R = [ordered]@{ AuditTimestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') Hostname = $env:COMPUTERNAME OSCaption = if ($os) { $os.Caption } else { $null } OSBuildFull = $osBuildFull EnvironmentType = Get-EnvironmentType Hardware = Get-HardwareInfo SecureBoot = Get-SecureBootState OperatingMode = 'Unknown' BitLocker = $null Certificates = $null Registry = Get-RegistryStatus EventLog = Get-EventLogStatus TaskExists = Get-TaskExists Category = $null CategoryLabel = $null CategoryColor = $null } if ($R.SecureBoot.IsUEFI -and $R.SecureBoot.IsSupported) { $R.OperatingMode = Get-SecureBootMode $R.Certificates = Get-CertificateStatus } else { $R.Certificates = [ordered]@{ KEK = [ordered]@{ Has2011 = $false; Has2023 = $false Certs2011 = @(); Certs2023 = @() Error = 'Secure Boot nedostupný' } DB = [ordered]@{ Has2011UEFI = $false; Has2011WindowsPCA = $false Has2023UEFI = $false; Has2023OptionROM = $false Has2023WindowsUEFI = $false Certs2011 = @(); Certs2023 = @() Error = 'Secure Boot nedostupný' } DbxRevokesPCA2011 = $false AnyExpiring2011 = $false } } $R.BitLocker = Get-BitLockerInfo $cat = Get-RemediationCategory -Result $R $R.Category = $cat.Code $R.CategoryLabel = $cat.Label $R.CategoryColor = $cat.Color return $R } #endregion #region ── Zobrazení ────────────────────────────────────────────────────────── function Show-Prerequisites { param($Prereqs) Write-Head 'PŘEDPOKLADY' $i = 1 foreach ($p in $Prereqs) { $state = if ($p.Ok) { 'Done' } elseif ($p.Hard) { 'Fail' } else { 'Info' } $nc = if ($p.Ok) { 'DarkGray' } elseif ($p.Hard) { 'Red' } else { 'Yellow' } Write-Check -State $state -Text ("{0}. {1}" -f $i, $p.Label) -Note $p.Note -NoteColor $nc $i++ } $hardFails = @($Prereqs | Where-Object { $_.Hard -and -not $_.Ok }) if ($hardFails.Count) { Write-Host '' Write-Host ' ! Před spuštěním remediace vyřešte výše označené problémy.' -ForegroundColor Red } else { Write-Host '' Write-Host ' Všechny předpoklady splněny.' -ForegroundColor Green } } function Show-Certificates { param($Cert) Write-Head 'CERTIFIKÁTY' # Najdi expiry datum pro přítomné povinné certifikáty $winUefiExpiry = $null $kekExpiry = $null foreach ($c in $Cert.DB.Certs2023) { if ($c.Subject -like "*$CN_WINUEFI2023*" -and -not $winUefiExpiry) { $winUefiExpiry = $c.NotAfter } } foreach ($c in $Cert.KEK.Certs2023) { if ($c.Subject -like "*$CN_KEK2023*" -and -not $kekExpiry) { $kekExpiry = $c.NotAfter } } $items = @( @{ Label='Windows UEFI CA 2023'; Present=$Cert.DB.Has2023WindowsUEFI; Required=$true; NotAfter=$winUefiExpiry } @{ Label='Microsoft Corporation KEK 2K CA 2023'; Present=$Cert.KEK.Has2023; Required=$true; NotAfter=$kekExpiry } @{ Label='Microsoft UEFI CA 2023'; Present=$Cert.DB.Has2023UEFI; Required=$false; NotAfter=$null } @{ Label='Microsoft Option ROM UEFI CA 2023'; Present=$Cert.DB.Has2023OptionROM; Required=$false; NotAfter=$null } ) foreach ($item in $items) { $tag = if ($item.Required) { '(povinný) ' } else { '(volitelný)' } $expiry = if ($item.NotAfter) { " platný do $($item.NotAfter)" } else { '' } if ($item.Present) { Write-Host (" [{0}] {1,-44} {2}{3}" -f $SYM_DONE, $item.Label, $tag, $expiry) -ForegroundColor Green Add-LogLine ("[+] $($item.Label) — přítomen$expiry") } elseif ($item.Required) { Write-Host (" [{0}] {1,-44} {2}" -f $SYM_FAIL, $item.Label, $tag) -ForegroundColor Red Add-LogLine ("[!] $($item.Label) — CHYBÍ (povinný)") } else { Write-Host (" [{0}] {1,-44} {2}" -f $SYM_PENDING, $item.Label, $tag) -ForegroundColor DarkGray Add-LogLine ("[ ] $($item.Label) — chybí (volitelný)") } } } function Show-Registry { param($Reg) Write-Head 'REGISTRY' $auText = Get-AvailableUpdatesText $Reg.AvailableUpdates $auVal = if ($null -ne $Reg.AvailableUpdates) { [int]$Reg.AvailableUpdates } else { -1 } $auColor = if ($auVal -eq 0 -or $auVal -eq 0x4000) { 'Green' } else { 'Cyan' } $stText = $Reg.UEFICA2023StatusText $stColor = switch ($stText) { 'Updated' { 'Green' } 'Failed' { 'Red' } 'KeyNotPresent' { 'DarkGray' } default { 'White' } } $errRaw = $Reg.UEFICA2023Error $errOk = ($null -eq $errRaw) -or ([int]$errRaw -eq 0) $errText = if ($errOk) { '(žádná)' } else { [string]$errRaw } $errColor = if ($errOk) { 'DarkGray' } else { 'Red' } $capText = $Reg.WindowsUEFICA2023CapableText $capColor = if ($Reg.WindowsUEFICA2023Capable -eq 2) { 'Green' } else { 'White' } Write-KV 'AvailableUpdates' $auText $auColor Write-KV 'UEFICA2023Status' $stText $stColor Write-KV 'UEFICA2023Error' $errText $errColor Write-KV 'WindowsUEFICA2023Capable' $capText $capColor } function Show-Events { param($Evt) Write-Head 'UDÁLOSTI (System log — Secure Boot)' if (-not $Evt.RelevantEvents -or $Evt.RelevantEvents.Count -eq 0) { Write-Host ' (žádné relevantní události)' -ForegroundColor DarkGray Add-LogLine 'Události: žádné relevantní' return } foreach ($ev in $Evt.RelevantEvents) { $evDesc = if ($EVENT_DESC.ContainsKey($ev.EventId)) { $EVENT_DESC[$ev.EventId] } else { $ev.Level } $color = if ($ev.EventId -in @(1795,1796)) { 'Red' } ` elseif ($ev.EventId -eq 1808) { 'Green' } ` else { 'DarkGray' } Write-Host (" {0} EventID {1,-4} {2}" -f $ev.TimeCreated, $ev.EventId, $evDesc) -ForegroundColor $color Add-LogLine (" [{0}] EventID {1}: {2}" -f $ev.TimeCreated, $ev.EventId, $evDesc) } } function Show-Status { # Zobrazí všechny 4 sekce + celkový stav. param($R, $Prereqs) Show-Prerequisites -Prereqs $Prereqs Show-Certificates -Cert $R.Certificates Show-Registry -Reg $R.Registry Show-Events -Evt $R.EventLog Write-Host '' Write-Host ' STAV: ' -ForegroundColor Gray -NoNewline Write-Host $R.CategoryLabel -ForegroundColor $R.CategoryColor Add-LogLine ("STAV: {0}" -f $R.CategoryLabel) } function Show-TwoRestartBlock { param($R) $bl = $R.BitLocker $blActive = $bl -and ($bl.Status -eq 'On' -or $bl.Status -eq '1' -or $bl.Status -eq '2') Write-Host '' Write-Host ' *** Boot Manager 2023 je připraven na disku (staged) ***' -ForegroundColor Yellow Write-Host '' Write-Host ' Pro dokončení jsou potřeba 2 kroky:' -ForegroundColor White Write-Host ' 1. RESTART — aktivuje nový Boot Manager' -ForegroundColor Gray Write-Host ' 2. Windows automaticky dokončí zbývající kroky po restartu' -ForegroundColor Gray Write-Host '' if ($blActive -and $bl.UsesPcr) { Write-Host ' BitLocker: Disk je chráněn s TPM+PCR7.' -ForegroundColor Yellow Write-Host ' Před restartem si připravte BitLocker recovery key!' -ForegroundColor Yellow } else { Write-Host ' BitLocker: Před restartem ověřte dostupnost recovery key.' -ForegroundColor Gray } Add-LogLine 'AU=0x4100 — zobrazen blok "2 kroky"' } #endregion #region ── Souhlas + restart ────────────────────────────────────────────────── function Get-UserConsent { param([string]$Question, [string]$Detail) if ($AssumeYes) { Write-Line (" {0} -> automaticky ANO (-AssumeYes)" -f $Question) Cyan return $true } if (-not [Environment]::UserInteractive) { Write-Line ' Neinteraktivní relace bez -AssumeYes — remediace se neprovede.' Yellow return $false } Write-Host '' Write-Host $Question -ForegroundColor White if ($Detail) { Write-Host $Detail -ForegroundColor DarkGray } try { $yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Ano', 'Aplikovat (bez restartu)' $no = New-Object System.Management.Automation.Host.ChoiceDescription '&Ne', 'Neprovádět změny' return ($Host.UI.PromptForChoice('', 'Vaše volba:', [System.Management.Automation.Host.ChoiceDescription[]]@($yes,$no), 1) -eq 0) } catch { return ((Read-Host 'Aplikovat? [a/N]') -match '^(a|ano|y|yes)$') } } function Invoke-RestartOffer { param([bool]$Auto) if ($Auto) { Write-Host '' Write-Host ' AutoRestart: restartuji server nyní...' -ForegroundColor Yellow Add-LogLine 'Restart: -AutoRestart — spouštím Restart-Computer' Flush-Log Restart-Computer -Force return } if (-not [Environment]::UserInteractive) { Write-Line ' (Neinteraktivní relace — restart proveďte ručně.)' DarkGray return } Write-Host '' Write-Host ' Chcete nyní restartovat server? (výchozí: Ne)' -ForegroundColor White try { $yn = New-Object System.Management.Automation.Host.ChoiceDescription '&Ano', 'Restartovat server nyní' $nn = New-Object System.Management.Automation.Host.ChoiceDescription '&Ne', 'Restartovat ručně' if ($Host.UI.PromptForChoice('', ' Restart:', [System.Management.Automation.Host.ChoiceDescription[]]@($yn,$nn), 1) -eq 0) { Add-LogLine 'Restart: uživatel potvrdil — spouštím Restart-Computer' Flush-Log Restart-Computer -Force } else { Write-Host ' Restart odložen — restartujte server ručně co nejdříve.' -ForegroundColor Gray Add-LogLine 'Restart: uživatel odložil restart' } } catch { if ((Read-Host ' Restartovat nyní? [a/N]') -match '^(a|ano|y|yes)$') { Add-LogLine 'Restart: uživatel potvrdil restart (fallback Read-Host)' Flush-Log Restart-Computer -Force } else { Write-Line ' Restart odložen — restartujte server ručně co nejdříve.' Gray Add-LogLine 'Restart: uživatel odložil restart (fallback)' } } } #endregion #region ── Předpoklady ──────────────────────────────────────────────────────── function Get-Prerequisites { param($R, [bool]$IsAdmin) $sb = $R.SecureBoot $list = @() # 1. Administrátorská práva $list += @{ Label = 'Administrátorská práva' Ok = $IsAdmin; Hard = $true Note = if (-not $IsAdmin) { 'Spusťte PowerShell "Run as administrator"' } else { $null } } # 2. Windows Server $isServer = $R.OSCaption -like '*Windows Server*' $list += @{ Label = ("Windows Server (2016/2019/2022/2025) — Zjištěno: {0} (build {1})" -f $R.OSCaption, $R.OSBuildFull) Ok = $isServer; Hard = $true Note = if (-not $isServer) { 'Skript je určen pro Windows Server' } else { $null } } # 3. UEFI + Secure Boot zapnutý $sbAllOk = $sb.IsUEFI -and $sb.IsSupported -and $sb.IsEnabled $sbNote = if (-not $sb.IsUEFI) { 'Legacy BIOS — Secure Boot není k dispozici' } ` elseif (-not $sb.IsSupported) { 'Secure Boot není podporováno' } ` elseif (-not $sb.IsEnabled) { 'Secure Boot je vypnutý — zapněte v UEFI/BIOS' } ` else { $null } $list += @{ Label = 'UEFI + Secure Boot zapnutý' Ok = $sbAllOk; Hard = $true; Note = $sbNote } # 4. Stavové registry klíče přítomny (servicing klíč = záplata >= 10/2025) $servNote = if (-not $R.Registry.ServicingKeyExists) { "build $($R.OSBuildFull) — nainstalujte kumulativní update min. z 10/2025 (KB5066835)" } else { $null } $list += @{ Label = 'Úroveň záplat >= 10/2025 — stavové registry klíče přítomny' Ok = $R.Registry.ServicingKeyExists; Hard = $true; Note = $servNote } # 5. Scheduled task Secure-Boot-Update $taskNote = if (-not $R.TaskExists) { "Task '$TASK_NAME' chybí — nainstalujte kumulativní update min. z 10/2025" } else { $null } $list += @{ Label = ("Scheduled task '$TASK_NAME' k dispozici") Ok = [bool]$R.TaskExists; Hard = $true; Note = $taskNote } # 6. PowerShell SecureBoot cmdlety $sbCmdletOk = $null -ne (Get-Command Get-SecureBootUEFI -ErrorAction SilentlyContinue) $list += @{ Label = 'PowerShell SecureBoot cmdlety — Get-SecureBootUEFI dostupný' Ok = $sbCmdletOk; Hard = $true Note = if (-not $sbCmdletOk) { 'Cmdlet nedostupný — možná Legacy BIOS nebo chybí modul' } else { $null } } return $list } #endregion #region ── Remediace ────────────────────────────────────────────────────────── function Invoke-Remediation { $out = [ordered]@{ Status='Unknown'; Message=''; After=$null } Write-Head 'REMEDIACE' Add-LogLine 'REMEDIACE START' # Krok 1/3: Nastavit registry # AvailableUpdates = 0x5944 pouze jednou — při prvním spuštění (null) nebo po dokončení (0). # Systém hodnotu sám spravuje: 0x5944→0x5904→0x5104→0x4104→0x4100→0x4000→0x0. $auCurrent = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue).AvailableUpdates $auNeedsInit = ($null -eq $auCurrent) -or ([int]$auCurrent -eq 0) $auInitLabel = if ($auNeedsInit) { '0x5944 (první inicializace)' } else { '0x{0:X} (zachováno, probíhá)' -f [int]$auCurrent } Write-Host (" [1/3] Nastavuji registry (AvailableUpdates={0}) ... " -f $auInitLabel) -NoNewline try { if (-not (Test-Path $REG_SECUREBOOT)) { New-Item -Path $REG_SECUREBOOT -Force -ErrorAction Stop | Out-Null } Set-ItemProperty -Path $REG_SECUREBOOT -Name 'MicrosoftUpdateManagedOptIn' -Value 1 -Type DWord -Force -ErrorAction Stop if ($auNeedsInit) { Set-ItemProperty -Path $REG_SECUREBOOT -Name 'AvailableUpdates' -Value $AVAILABLE_UPDATES_VALUE -Type DWord -Force -ErrorAction Stop $auCheck = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction Stop).AvailableUpdates if ([int]$auCheck -ne $AVAILABLE_UPDATES_VALUE) { throw ('Ověření selhalo — AvailableUpdates=0x{0:X}, očekáváno 0x5944' -f [int]$auCheck) } } $optOut = Get-ItemProperty $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -ErrorAction SilentlyContinue if ($optOut -and $optOut.HighConfidenceOptOut -ne 0) { Set-ItemProperty -Path $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -Value 0 -Type DWord -Force -ErrorAction Stop } if (-not (Test-Path $REG_SERVICING)) { New-Item -Path $REG_SERVICING -Force -ErrorAction Stop | Out-Null } Write-Host 'OK' -ForegroundColor Green Add-LogLine ("Krok 1: MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}" -f $auInitLabel) } catch { Write-Host 'CHYBA' -ForegroundColor Red Write-Line (" $($_.Exception.Message)") Red $out.Status = 'Error' $out.Message = "Zápis registry selhal: $($_.Exception.Message)" return $out } # Krok 2/3: Spustit servicing task a čekat na změnu stavu Write-Host ' [2/3] Spouštím servicing task ' -NoNewline $task = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue if (-not $task) { Write-Host '— task nenalezen.' -ForegroundColor Yellow Write-Line ' Registry nastavena; Windows zpracuje při servisním běhu.' DarkGray Add-LogLine 'Krok 2: task nenalezen — přeskočeno' } else { $initAU = [int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) try { Start-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop Start-Sleep -Seconds 2 $elapsed = 0 $state = 'Running' $changed = $false do { Start-Sleep -Seconds 3 $elapsed += 3 Write-Host '.' -NoNewline -ForegroundColor DarkGray $state = (Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue).State $nowAU = [int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) if ($nowAU -ne $initAU) { $changed = $true; break } } while ($state -eq 'Running' -and $elapsed -lt $TASK_TIMEOUT_SEC) $finalAU = [int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) if ($changed) { Write-Host (" OK ({0}s, AvailableUpdates -> 0x{1:X})" -f $elapsed, $finalAU) -ForegroundColor Green } elseif ($elapsed -ge $TASK_TIMEOUT_SEC) { Write-Host (" timeout {0}s (stav: {1})" -f $elapsed, $state) -ForegroundColor Yellow } else { Write-Host (" OK ({0}s, stav: {1})" -f $elapsed, $state) -ForegroundColor Green } Add-LogLine ("Krok 2: elapsed={0}s, stav={1}, AvailableUpdates=0x{2:X}" -f $elapsed, $state, $finalAU) } catch { Write-Host '— nepodařilo se spustit.' -ForegroundColor Yellow Write-Line (" $($_.Exception.Message)") DarkGray } } # Krok 3/3: Ověřit nový stav Write-Host ' [3/3] Ověřuji nový stav ... ' -NoNewline Start-Sleep -Seconds 2 $after = Invoke-Detection Write-Host 'OK' -ForegroundColor Green $out.Status = 'Applied' $out.After = $after $out.Message = 'Registry nastavena, task zpracován.' Add-LogLine 'REMEDIACE KONEC: Applied' return $out } #endregion #region ── Stav napříč restarty ─────────────────────────────────────────────── function Save-ResumeState { param($R, [int]$Cycle) try { if (-not (Test-Path $WORK_ROOT)) { New-Item -Path $WORK_ROOT -ItemType Directory -Force | Out-Null } @{ ComputerName = $env:COMPUTERNAME Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm') Cycle = $Cycle Category = $R.Category AvailableUpdates = ('0x{0:X}' -f [int]$R.Registry.AvailableUpdates) } | ConvertTo-Json | Set-Content -LiteralPath $STATE_FILE -Encoding UTF8 } catch { } } function Get-ResumeState { if (-not (Test-Path $STATE_FILE)) { return $null } try { return (Get-Content -LiteralPath $STATE_FILE -Raw | ConvertFrom-Json) } catch { return $null } } function Clear-ResumeState { if (Test-Path $STATE_FILE) { Remove-Item -LiteralPath $STATE_FILE -Force -ErrorAction SilentlyContinue } } #endregion #region ── Main ─────────────────────────────────────────────────────────────── $isWhatIf = [bool]$WhatIfPreference # Hlavička Write-Host '' Write-Rule 'Cyan' Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor White Write-Host (" {0,-40} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray Write-Rule 'Cyan' $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) Add-LogLine ('===== SPUŠTĚNO {0} | {1} | admin={2} | CheckOnly={3} AssumeYes={4} Force={5} AutoRestart={6} =====' -f ` $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $isAdmin, ` $CheckOnly.IsPresent, $AssumeYes.IsPresent, $Force.IsPresent, $AutoRestart.IsPresent) $prev = Get-ResumeState if ($prev) { Write-Host '' Write-Host (" Navazuji na předchozí běh (cyklus #{0}, {1}, stav: {2})." -f ` $prev.Cycle, $prev.Timestamp, $prev.Category) -ForegroundColor DarkCyan } # Detekce stavu Write-Host '' Write-Host ' Zjišťuji stav serveru...' -ForegroundColor DarkGray $result = Invoke-Detection $prereqs = Get-Prerequisites -R $result -IsAdmin $isAdmin # Zobrazení stavu (4 sekce: předpoklady, certifikáty, registry, události) Show-Status -R $result -Prereqs $prereqs # ── CheckOnly: konec bez akce ───────────────────────────────────────────────── if ($CheckOnly) { Write-Host ''; Write-Rule $exitCode = switch -Wildcard ($result.Category) { 'OK*' { 0 } 'UPDATE_*' { 1 } default { 2 } } Write-Host (" CHECK: {0} (exit {1})" -f $result.CategoryLabel, $exitCode) -ForegroundColor $result.CategoryColor Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray Save-ResumeState -R $result -Cycle $(if ($prev) { [int]$prev.Cycle } else { 0 }) Flush-Log exit $exitCode } # ── Ověřit hard prerekvizity ───────────────────────────────────────────────── $hardFails = @($prereqs | Where-Object { $_.Hard -and -not $_.Ok }) if ($hardFails.Count) { Write-Host '' Write-Host ' Nelze pokračovat — vyřešte výše označené problémy a spusťte skript znovu.' -ForegroundColor Red Flush-Log exit 2 } # ── Blokující kategorie ─────────────────────────────────────────────────────── $blockedMap = @{ 'NO_SECUREBOOT' = 'Secure Boot není podporováno (Legacy BIOS). Dokumentujte server jako výjimku.' 'NO_SECUREBOOT_VM' = 'Secure Boot není podporováno (VM bez UEFI nebo vTPM). Dokumentujte jako výjimku.' 'SECUREBOOT_DISABLED' = 'Secure Boot je vypnutý. Zapnutí je mimo rozsah tohoto skriptu — nastavte v UEFI/BIOS.' 'SETUP_MODE' = 'Secure Boot je v Setup Mode. V UEFI/BIOS obnovte Secure Boot klíče (enroll PK) a spusťte skript znovu.' 'NOT_SUPPORTED' = 'Zařízení nepodporuje automatickou aktualizaci (ConfidenceLevel: Not Supported). Kontaktujte výrobce firmware.' 'BUILD_OUTDATED' = 'Tento build Windows neumí nasadit 2023 certifikáty. Nainstalujte Windows Update (min. 10/2025, KB5066835) a restartujte.' } if ($blockedMap.ContainsKey($result.Category)) { Write-Host '' Write-Host (" {0}" -f $blockedMap[$result.Category]) -ForegroundColor Yellow Save-ResumeState -R $result -Cycle $(if ($prev) { [int]$prev.Cycle } else { 0 }) Flush-Log exit 2 } if ($result.Category -eq 'FIRMWARE_UPDATE_NEEDED') { Write-Host '' Write-Host (" Aktualizace pozastavena — firmware serveru {0} ji nepodporuje." -f $result.Hardware.Manufacturer) -ForegroundColor Yellow Write-Host ' Aktualizujte firmware u výrobce a poté spusťte skript znovu.' -ForegroundColor Gray Save-ResumeState -R $result -Cycle $(if ($prev) { [int]$prev.Cycle } else { 0 }) Flush-Log exit 2 } # ── UPDATE_FAILED — upozornit, ale přesto nabídnout opakování ──────────────── if ($result.Category -eq 'UPDATE_FAILED') { Write-Host '' $errEvtId = $result.Registry.UEFICA2023ErrorEvent if ($errEvtId -and [int]$errEvtId -ne 0) { Write-Host (" Předchozí pokus selhal (UEFICA2023ErrorEvent={0}). Zkuste znovu nebo aktualizujte firmware." -f $errEvtId) -ForegroundColor Red } elseif ($result.EventLog.ById[1795]) { Write-Host (" Selhání firmwaru (EventID 1795, {0}). Aktualizujte firmware nebo Hyper-V hostitele (KB5085790)." -f $result.EventLog.ById[1795].Time) -ForegroundColor Red } else { Write-Host ' Předchozí pokus selhal. Zkuste spustit remediaci znovu.' -ForegroundColor Red } } $cycle = if ($prev) { [int]$prev.Cycle } else { 0 } # ── Předběžná kontrola: povinné 2023 certy přítomny? ───────────────────────── # Pokud ano a není -Force → ukončit bez remediace. $certsReady = $result.Certificates.KEK.Has2023 -and $result.Certificates.DB.Has2023WindowsUEFI if ($certsReady -and ($result.Category -like 'OK*') -and -not $Force) { Write-Host '' Write-Host ' Povinné 2023 certifikáty (Windows UEFI CA 2023 + KEK 2K CA 2023) jsou' -ForegroundColor Green Write-Host ' již přítomny v UEFI databázích. Na tomto serveru není nutná žádná akce.' -ForegroundColor Green Write-Host ' (Tip: parametr -Force vynutí remediaci i v tomto stavu.)' -ForegroundColor DarkGray Write-Host ''; Write-Rule 'Cyan' Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray Write-Rule 'Cyan'; Write-Host '' if ($result.Category -like 'OK*') { Clear-ResumeState } else { Save-ResumeState -R $result -Cycle $cycle } Flush-Log exit 0 } if ($certsReady -and $Force) { Write-Host '' Write-Host ' ! Certifikáty jsou přítomny, ale -Force umožňuje spustit remediaci znovu.' -ForegroundColor Yellow } # ── AU=0x4100: Boot Manager staged, stačí RESTART ──────────────────────────── $auNow = [int]$result.Registry.AvailableUpdates if ($auNow -eq 0x4100) { Show-TwoRestartBlock -R $result if (-not $isWhatIf) { Invoke-RestartOffer -Auto:$AutoRestart.IsPresent } Write-Host ''; Write-Rule 'Cyan' Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray Write-Rule 'Cyan'; Write-Host '' Save-ResumeState -R $result -Cycle $cycle Flush-Log exit 1 } # ── Zkontrolovat administrátorská práva před remediací ──────────────────────── if (-not $isAdmin) { Write-Host '' Write-Host ' Remediaci nelze provést bez práv Administrator.' -ForegroundColor Yellow Write-Host ' Spusťte PowerShell "Run as administrator" a zkuste znovu.' -ForegroundColor Gray Flush-Log exit 2 } # ── WhatIf: zobrazit co by se stalo ───────────────────────────────────────── $remediation = $null if ($isWhatIf) { Write-Host '' Write-Host ' -WhatIf — co by remediace udělala (bez změn):' -ForegroundColor Cyan Write-Host ' - Nastaví MicrosoftUpdateManagedOptIn=1, HighConfidenceOptOut=0' -ForegroundColor Gray $auWhatIfLabel = if ($auNow -eq 0) { '0x5944 (první inicializace)' } else { '0x{0:X} (zachováno)' -f $auNow } Write-Host (" - AvailableUpdates: {0}" -f $auWhatIfLabel) -ForegroundColor Gray Write-Host ' - Spustí servicing task a bude čekat na změnu stavu' -ForegroundColor Gray $autoRestartNote = if ($AutoRestart) { 'ANO (-AutoRestart)' } else { 'NE (nabídne dotaz)' } Write-Host (" - Restart serveru: {0}" -f $autoRestartNote) -ForegroundColor Gray } else { # ── Nabídnout remediaci ────────────────────────────────────────────────── $question = switch ($result.Category) { 'UPDATE_NEEDED' { 'Server potřebuje aktualizaci Secure Boot certifikátů. Chcete zahájit proces?' } 'UPDATE_PARTIAL' { 'Aktualizace probíhá — část certifikátů je nasazena. Spustit task pro posun procesu?' } 'UPDATE_PENDING_RESTART' { 'Certifikáty jsou v procesu. Spustit servicing task?' } 'UPDATE_FAILED' { 'Předchozí pokus selhal. Chcete zkusit remediaci znovu?' } default { 'Chcete spustit remediaci Secure Boot certifikátů?' } } $detail = 'Nastaví registry (KB5068202) a spustí servicing task. Server NEBUDE restartován (bez -AutoRestart).' if (Get-UserConsent -Question $question -Detail $detail) { $cycle++ Add-LogLine ("Cyklus #{0} | {1} | kategorie={2}" -f $cycle, $result.Hostname, $result.Category) $remediation = Invoke-Remediation Flush-Log } else { Write-Host '' Write-Host ' Remediace neprovedena — spusťte skript znovu, až budete připraveni.' -ForegroundColor Yellow } } # ── Závěr ───────────────────────────────────────────────────────────────────── Write-Host ''; Write-Rule 'Cyan' if ($remediation -and $remediation.Status -eq 'Applied') { $after = $remediation.After $afterAU = [int]$after.Registry.AvailableUpdates $afterDone = Test-RemediationComplete -R $after Write-Host ' Stav po remediaci:' -ForegroundColor Cyan Show-Certificates -Cert $after.Certificates Show-Registry -Reg $after.Registry Write-Host '' if ($afterDone) { Write-Host (" [{0}] HOTOVO — Secure Boot certifikáty 2023 úspěšně nasazeny!" -f $SYM_DONE) -ForegroundColor Green Write-Host ' Podmínky splněny: certifikáty v DB/KEK, UEFICA2023Status=Updated, AvailableUpdates=0x0/0x4000.' -ForegroundColor Green Clear-ResumeState } elseif ($afterAU -eq 0x4100) { Show-TwoRestartBlock -R $after Invoke-RestartOffer -Auto:$AutoRestart.IsPresent Save-ResumeState -R $after -Cycle $cycle } else { Write-Host ' Remediace proběhla — proces vyžaduje restart pro dokončení.' -ForegroundColor Yellow Write-Host '' Write-Host ' Další kroky:' -ForegroundColor White Write-Host ' 1. Restartujte server.' -ForegroundColor Gray Write-Host ' 2. Spusťte skript znovu — opakujte, dokud nebude HOTOVO.' -ForegroundColor Gray Write-Host (" 3. Cíl: AvailableUpdates=0x0, UEFICA2023Status=Updated, certifikáty v KEK+DB.") -ForegroundColor DarkGray Save-ResumeState -R $after -Cycle $cycle } } elseif ($remediation -and $remediation.Status -eq 'Error') { Write-Host (" CHYBA REMEDIACE — {0}" -f $remediation.Message) -ForegroundColor Red Save-ResumeState -R $result -Cycle $cycle } else { # Bez remediace (odmítnutí / WhatIf / blocked) Write-Host ' KONEC — bez změn na serveru.' -ForegroundColor Cyan $finalR = $result if ($finalR.Category -like 'OK*') { Clear-ResumeState } else { Save-ResumeState -R $finalR -Cycle $cycle } } Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray Write-Rule 'Cyan'; Write-Host '' Flush-Log #endregion