diff --git a/Invoke-SecureBootRemediation.ps1 b/Invoke-SecureBootRemediation.ps1 index 64dbc81..a5c4bfe 100644 --- a/Invoke-SecureBootRemediation.ps1 +++ b/Invoke-SecureBootRemediation.ps1 @@ -15,12 +15,12 @@ - ConfidenceLevel (REG_SZ): High Confidence / Temporarily Paused / Not Supported / ... - AvailableUpdates: 0x5944 -> 0x5904 -> 0x5104 -> 0x4104 -> 0x4100 -> 0x4000 -> 0x0 - Cílový stav: AvailableUpdates=0x0, UEFICA2023Status=Updated, WindowsUEFICA2023Capable=2 - - Events (TPM-WMI): 1808=hotovo, 1799=boot mgr 2023, 1795=chyba firmwaru, - 1802=pozastaveno, 1803=odloženo (monitor), 1801=zatím neaplikováno (monitor) + - Events (TPM-WMI): 1795=chyba firmwaru, 1796=selhání zápisu UEFI var, + 1802=pozastaveno (firmware), 1803=odloženo (monitor), 1801=čeká (monitor) - Skript ZÁMĚRNĚ NERESTARTUJE server. Sleduje stav i napříč restarty (state.json) a umí - volitelně po dalším restartu spustit kontrolu automaticky (-RegisterResume) — stále BEZ - auto-restartu. + Skript nabídne restart (interaktivní dotaz) jen při AvailableUpdates=0x4100 (Boot Manager + staged). Ve všech ostatních stavech task spustí a posune hodnotu. Sleduje stav napříč + restarty (state.json). Volitelně registruje RunOnce pro auto-check po restartu (-RegisterResume). .PARAMETER CheckOnly Jen detekce + checklist + exit kód (0=hotovo, 1=nutná akce, 2=blokováno). Bez změn/dotazu. @@ -35,8 +35,8 @@ Nastaví registry, ale nespustí servicing task (spustí se sám cca á 12 h). .PARAMETER SkipBootManagerFileCheck - Neověřuje bootmgfw.efi na ESP přes mount+certutil. Dokončení se i tak pozná z - WindowsUEFICA2023Capable=2 / Event 1808 / 1799. + Neověřuje bootmgfw.efi na ESP přes mount+certutil. Dokončení se pozná z + WindowsUEFICA2023Capable=2 a UEFICA2023Status=Updated. .PARAMETER RegisterResume Po remediaci, kde zbývá restart, zaregistruje RunOnce, který po PŘÍŠTÍM (ručním) restartu @@ -123,7 +123,6 @@ $SYM_PENDING = ' ' #region ── Log / barevný výstup ─────────────────────────────────────────────── -$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } # Jeden společný log na LOKÁLNÍM disku serveru (%ProgramData%), připojovaný pod sebe přes běhy. # Záměrně NE vedle skriptu — ten může být RDP-redirected disk, kde per-řádkový zápis vytváří # poškozené (null) soubory. Řádky se bufferují a zapíšou jednorázově (Flush-Log), což je odolné. @@ -243,25 +242,44 @@ function Get-BitLockerInfo { #region ── Detekce — certifikáty / registry / events / boot manager ─────────── 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, v každé seznam SignatureEntry s 16B GUID prefixem. param([byte[]]$Bytes) - $certs=@(); if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs } - $G=[byte[]](0xa1,0x59,0xc0,0xa5,0xe4,0x94,0xa7,0x4a,0x87,0xb5,0xab,0x15,0x5c,0x2b,0xf0,0x72) - $o=0 - while ($o + 28 -le $Bytes.Length) { - $gid=$Bytes[$o..($o+15)]; $listSize=[BitConverter]::ToUInt32($Bytes,$o+16); $hdr=[BitConverter]::ToUInt32($Bytes,$o+20); $sz=[BitConverter]::ToUInt32($Bytes,$o+24) - if ($listSize -lt 28 -or $listSize -gt ($Bytes.Length-$o)) { break } - $x=$true; for ($i=0;$i -lt 16;$i++){ if ($gid[$i] -ne $G[$i]){ $x=$false; break } } - if ($x -and $sz -gt 16) { - $so=$o+28+$hdr; $end=$o+$listSize - while ($so+$sz -le $end) { - $co=$so+16; $cs=[int]$sz-16 - if ($co+$cs -le $Bytes.Length -and $cs -gt 0) { - try { $certs += New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(, [byte[]]($Bytes[$co..($co+$cs-1)])) } catch { } + $certs = @() + if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs } + + # GUID pro EFI_CERT_X509_GUID: {a5c059a1-94e4-4aa7-87b5-ab155c2bf072} (little-endian bytes) + $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 } + + # Zpracovat pouze X.509 záznamy + $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 { } } - $so += $sz + $entryPos += $sigSize } } - $o += $listSize + $pos += $listSize } return $certs } @@ -281,20 +299,25 @@ function Get-CertificateStatus { DbxRevokesPCA2011=$false; AnyExpiring2011=$false } try { - foreach ($c in (Parse-EFISignatureList -Bytes (Get-SecureBootUEFI -Name KEK -ErrorAction Stop).Bytes)) { - $s=$c.Subject; $info=Convert-CertToInfo $c - if ($s -like '*KEK CA 2011*') { $st.KEK.Has2011=$true; $st.KEK.Certs2011+=$info } - if ($s -like "*$CN_KEK2023*" -or $s -like '*KEK*CA 2023*') { $st.KEK.Has2023=$true; $st.KEK.Certs2023+=$info } + $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 { - foreach ($c in (Parse-EFISignatureList -Bytes (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes)) { - $s=$c.Subject; $info=Convert-CertToInfo $c - if ($s -like '*UEFI CA 2011*') { $st.DB.Has2011UEFI=$true; $st.DB.Certs2011+=$info } - if ($s -like '*Windows Production PCA 2011*' -or $s -like '*Windows PCA 2011*') { $st.DB.Has2011WindowsPCA=$true; $st.DB.Certs2011+=$info } - if ($s -like "*$CN_UEFI2023*" -and $s -notlike '*Option ROM*' -and $s -notlike '*Windows UEFI*') { $st.DB.Has2023UEFI=$true; $st.DB.Certs2023+=$info } - if ($s -like "*$CN_OPTROM2023*") { $st.DB.Has2023OptionROM=$true; $st.DB.Certs2023+=$info } - if ($s -like "*$CN_WINUEFI2023*") { $st.DB.Has2023WindowsUEFI=$true; $st.DB.Certs2023+=$info } + $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 (kdyby X.509 parse selhal) — jen doplní booleany @@ -348,8 +371,8 @@ function Get-AvailableUpdatesText { if ($null -eq $v) { return '(nenastaveno)' } $iv=[int]$v; $hex='0x{0:X}' -f $iv $n = switch ($iv) { - 0 {'vše hotovo (proces dokončen)'} 0x4000 {'vše aplikováno — čeká na finální restart'} - 0x4100 {'boot manager 2023 nasazen na ESP'} 0x4104 {'Microsoft UEFI CA 2023 v DB'} + 0 {'vše hotovo (proces dokončen)'} 0x4000 {'vše aplikováno — task čistí guard bit'} + 0x4100 {'boot manager 2023 nasazen na ESP — čeká na 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 {'zbývá aplikovat'} } return "$hex ($n)" @@ -362,9 +385,22 @@ function Get-EventLogStatus { $events = Get-WinEvent -FilterHashtable @{ LogName='System'; Id=$ids } -MaxEvents 40 -ErrorAction Stop if ($events) { $sorted = $events | Sort-Object TimeCreated -Descending - foreach ($id in $ids) { $x=$sorted | Where-Object {$_.Id -eq $id} | Select-Object -First 1; if ($x){ $e.ById[$id]=@{Time=$x.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')} } } - foreach ($x in ($sorted|Select-Object -First 8)) { $e.RelevantEvents += [ordered]@{ EventId=$x.Id; TimeCreated=$x.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); Level=$x.LevelDisplayName } } - $last=$sorted|Select-Object -First 1; $e.LastEventId=$last.Id; $e.LastEventTime=$last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') + # Index posledního výskytu každého ID + 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') } } + } + # Posledních 8 událostí pro -Detailed výpis + foreach ($ev in ($sorted | Select-Object -First 8)) { + $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 } @@ -412,19 +448,27 @@ function Build-Phases { param($Sb, $Cert, $Boot, $TaskExists, $Reg, $Evt) $e1795=$Evt.ById[1795]; $e1796=$Evt.ById[1796] $cap=$Reg.WindowsUEFICA2023Capable - $bmActive = ($cap -eq 2) -or ($null -ne $Evt.ById[1808]) -or ($null -ne $Evt.ById[1799]) + # Pouze Capable=2 je autoritativní signál aktivního Boot Manageru 2023. + # Eventy 1808/1799 nejsou Done signálem — mohou být ze starých/předchozích běhů a jsou nespolehlivé. + $bmActive = ($cap -eq 2) $bmStaged = [bool]$Boot.EspHas2023 + # UEFICA2023Status musí být "Updated" pro potvrzení, že cert servisování proběhlo. + # Pokud klíč zcela chybí (starý build / BeforeServicing), netlumíme — BUILD_OUTDATED to zachytí jinak. + $statusUpdated = ($Reg.UEFICA2023StatusText -eq 'Updated') + $statusAbsent = ($null -eq $Reg.UEFICA2023Status) + $certServicingDone = $statusUpdated -or $statusAbsent $ph=[ordered]@{} function _p($req,$done,$label){ [ordered]@{ Req=$req; Done=[bool]$done; Label=$label; State=$null; Note=$null } } - $ph['SecureBootEnabled'] = _p $true $Sb.IsEnabled 'Secure Boot zapnutý' - $ph['TaskExists'] = _p $true $TaskExists 'Servicing task k dispozici' - $ph['Kek2023'] = _p $true $Cert.KEK.Has2023 "KEK: Microsoft Corporation $CN_KEK2023" - $ph['Db2023Windows'] = _p $true $Cert.DB.Has2023WindowsUEFI "DB: $CN_WINUEFI2023" - $ph['BootManager2023'] = _p $true $bmActive 'Boot Manager aktivní (bootuje se z Windows UEFI CA 2023)' - $ph['Db2023ThirdParty'] = _p $false $Cert.DB.Has2023UEFI "DB: $CN_UEFI2023 (3rd-party, volitelné)" - $ph['Db2023OptionRom'] = _p $false $Cert.DB.Has2023OptionROM "DB: $CN_OPTROM2023 (volitelné)" - $ph['DbxRevoked'] = _p $false $Cert.DbxRevokesPCA2011 'DBX: revokace starého boot manageru 2011 (volitelné)' + $ph['SecureBootEnabled'] = _p $true $Sb.IsEnabled 'Secure Boot zapnutý' + $ph['TaskExists'] = _p $true $TaskExists 'Servicing task k dispozici' + $ph['Kek2023'] = _p $true $Cert.KEK.Has2023 "KEK: Microsoft Corporation $CN_KEK2023" + $ph['Db2023Windows'] = _p $true $Cert.DB.Has2023WindowsUEFI "DB: $CN_WINUEFI2023" + $ph['CertServicingStatus'] = _p $true $certServicingDone 'Certifikáty aktualizovány (UEFICA2023Status=Updated)' + $ph['BootManager2023'] = _p $true $bmActive 'Boot Manager aktivní (bootuje se z Windows UEFI CA 2023)' + $ph['Db2023ThirdParty'] = _p $false $Cert.DB.Has2023UEFI "DB: $CN_UEFI2023 (3rd-party, volitelné)" + $ph['Db2023OptionRom'] = _p $false $Cert.DB.Has2023OptionROM "DB: $CN_OPTROM2023 (volitelné)" + $ph['DbxRevoked'] = _p $false $Cert.DbxRevokesPCA2011 'DBX: revokace starého boot manageru 2011 (volitelné)' $kekDb = $ph['Kek2023'].Done -and $ph['Db2023Windows'].Done $arrow = [char]0x2190 @@ -438,10 +482,26 @@ function Build-Phases { elseif ($e1795) { $p.State='Fail'; $p.Note='chyba firmwaru (Event 1795)' } else { $p.State='Pending'; $p.Note='zbývá nasadit' } } + 'CertServicingStatus' { + $p.State = 'Pending' + if ($Reg.UEFICA2023StatusText -eq 'InProgress') { + $p.Note = 'servisování probíhá — task ještě nedoběhl nebo čeká na restart' + } elseif ($Reg.UEFICA2023StatusText -eq 'NotStarted') { + $p.Note = 'task zatím nezahájil servisování certifikátů' + } elseif ($Reg.UEFICA2023StatusText -eq 'KeyNotPresent') { + $p.Note = 'hodnota UEFICA2023Status není nastavena (starý build nebo před první remediací)' + } else { + $p.Note = "UEFICA2023Status: $($Reg.UEFICA2023StatusText)" + } + } 'BootManager2023' { - if ($bmStaged) { $p.State='Pending'; $p.Note="$arrow nasazen na ESP — vyžaduje RESTART" } - elseif ($kekDb) { $p.State='Pending'; $p.Note="$arrow zbývá — vyžaduje RESTART" } - else { $p.State='Pending'; $p.Note='až po nasazení KEK + DB' } + if ($bmStaged -or ($null -ne $cap -and [int]$cap -ge 1)) { + $p.State='Pending'; $p.Note="$arrow cert v DB (Capable=$cap) — aktivuje se RESTARTEM" + } elseif ($kekDb) { + $p.State='Pending'; $p.Note="$arrow zbývá — vyžaduje RESTART" + } else { + $p.State='Pending'; $p.Note='až po nasazení KEK + DB' + } } default { if ($e1795) { $p.State='Fail'; $p.Note='chyba firmwaru (Event 1795)' } else { $p.State='Pending'; $p.Note='zbývá nasadit' } @@ -456,14 +516,23 @@ function Get-RemediationCategory { $sb=$Result.SecureBoot; $reg=$Result.Registry; $evt=$Result.EventLog $env=$Result.EnvironmentType; $ph=$Result.Phases; $cert=$Result.Certificates; $mode=$Result.OperatingMode + # ── Základní blokátory ──────────────────────────────────────────────────── if (-not $sb.IsUEFI -or -not $sb.IsSupported) { - if ($env -like '*VM*') { return @{ Code='NO_SECUREBOOT_VM'; Tag='[X]'; Color='DarkGray'; Label='Secure Boot nepodporováno (VM bez vTPM/UEFI)' } } - return @{ Code='NO_SECUREBOOT'; Tag='[X]'; Color='DarkGray'; Label='Secure Boot nepodporováno (Legacy BIOS)' } + $lbl = if ($env -like '*VM*') { 'Secure Boot nepodporováno (VM bez vTPM/UEFI)' } else { 'Secure Boot nepodporováno (Legacy BIOS)' } + $cod = if ($env -like '*VM*') { 'NO_SECUREBOOT_VM' } else { 'NO_SECUREBOOT' } + return @{ Code=$cod; Tag='[X]'; Color='DarkGray'; Label=$lbl } + } + if (-not $sb.IsEnabled) { + return @{ Code='SECUREBOOT_DISABLED'; Tag='[OFF]'; Color='Yellow'; Label='Secure Boot vypnuto' } + } + if ($mode -eq 'Setup') { + return @{ Code='SETUP_MODE'; Tag='[!]'; Color='Magenta'; Label='Secure Boot v Setup Mode — aktualizaci nelze dokončit (chybí enrolled PK)' } + } + if (-not $ph['TaskExists'].Done) { + return @{ Code='TASK_MISSING'; Tag='[!]'; Color='Magenta'; Label='Chybí servicing task — nainstalujte aktuální kumulativní update (min. build 10/2025)' } } - if (-not $sb.IsEnabled) { return @{ Code='SECUREBOOT_DISABLED'; Tag='[OFF]'; Color='Yellow'; Label='Secure Boot vypnuto' } } - if ($mode -eq 'Setup') { return @{ Code='SETUP_MODE'; Tag='[!]'; Color='Magenta'; Label='Secure Boot v Setup Mode — aktualizaci nelze dokončit (chybí enrolled PK)' } } - if (-not $ph['TaskExists'].Done) { return @{ Code='TASK_MISSING'; Tag='[!]'; Color='Magenta'; Label='Chybí servicing task — nainstalujte aktuální kumulativní update (min. build 10/2025)' } } + # ── Chyby a blokování ───────────────────────────────────────────────────── $errEvt = $reg.UEFICA2023ErrorEvent -and ([int]$reg.UEFICA2023ErrorEvent -ne 0) if ($evt.ById[1795] -or $errEvt -or $reg.UEFICA2023StatusText -eq 'Failed') { return @{ Code='UPDATE_FAILED'; Tag='[FAIL]'; Color='Red'; Label='Selhání aktualizace (chyba firmwaru / UEFICA2023ErrorEvent)' } @@ -475,28 +544,38 @@ function Get-RemediationCategory { return @{ Code='FIRMWARE_UPDATE_NEEDED'; Tag='[FW]'; Color='Magenta'; Label='Aktualizace pozastavena (známý problém) — zkontrolujte firmware u OEM' } } - # Build příliš starý: DB/KEK už mají 2023, ale boot-manager servicing hodnoty vůbec neexistují - # (WindowsUEFICA2023Capable i UEFICA2023Status absentní) a Boot Manager není aktivní → OS nemá - # 2023-podepsaný boot manager ani jeho servicing. Restarty nepomohou, pomůže jen Windows Update. + # ── Starý build: certy jsou v KEK/DB, ale chybí servicing infrastruktura pro Boot Manager ── + # WindowsUEFICA2023Capable i UEFICA2023Status zcela chybí → Windows Update nutný. $certApplied = $ph['Kek2023'].Done -or $ph['Db2023Windows'].Done - $auNow = [int]$reg.AvailableUpdates - $bmStaged = ([bool]$Result.BootManager.EspHas2023) -or ($auNow -eq 0x4100) -or ($auNow -eq 0x4000) - if ($certApplied -and -not $ph['BootManager2023'].Done -and -not $bmStaged -and ($null -eq $reg.WindowsUEFICA2023Capable) -and ($null -eq $reg.UEFICA2023Status)) { + $auNow = [int]$reg.AvailableUpdates + $bmStaged = ([bool]$Result.BootManager.EspHas2023) -or ($auNow -eq 0x4100) -or ($auNow -eq 0x4000) + if ($certApplied -and -not $ph['BootManager2023'].Done -and -not $bmStaged ` + -and ($null -eq $reg.WindowsUEFICA2023Capable) -and ($null -eq $reg.UEFICA2023Status)) { return @{ Code='BUILD_OUTDATED'; Tag='[!]'; Color='Magenta'; Label='Build je příliš starý — chybí servicing pro Boot Manager 2023. Nutný Windows Update.' } } - $req=@('Kek2023','Db2023Windows','BootManager2023') - $missing=@($req|Where-Object{ -not $ph[$_].Done }); $missingLabels=@($missing|ForEach-Object{ $ph[$_].Label }) - $anyApplied = $ph['Kek2023'].Done -or $ph['Db2023Windows'].Done -or $ph['BootManager2023'].Done -or $cert.DB.Has2023UEFI -or $cert.DB.Has2023OptionROM -or [bool]$Result.BootManager.EspHas2023 + # ── Fázové vyhodnocení ──────────────────────────────────────────────────── + $req = @('Kek2023','Db2023Windows','CertServicingStatus','BootManager2023') + $missing = @($req | Where-Object { -not $ph[$_].Done }) + $missingLabels = @($missing | ForEach-Object { $ph[$_].Label }) + $anyApplied = $ph['Kek2023'].Done -or $ph['Db2023Windows'].Done -or $ph['BootManager2023'].Done ` + -or $cert.DB.Has2023UEFI -or $cert.DB.Has2023OptionROM -or [bool]$Result.BootManager.EspHas2023 if ($missing.Count -eq 0) { - if ($cert.AnyExpiring2011) { return @{ Code='OK_TRANSITION'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — 2023 certifikáty i aktivní Boot Manager (staré 2011 ještě přítomné, což je normální)' } } + if ($cert.AnyExpiring2011) { + return @{ Code='OK_TRANSITION'; Tag="[$SYM_DONE]"; Color='Green' + Label='HOTOVO — 2023 certifikáty i aktivní Boot Manager (staré 2011 ještě přítomné, což je normální)' } + } return @{ Code='OK'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — kompletní 2023 sada, aktivní Boot Manager, 2011 odstraněny' } } if ($missing.Count -eq 1 -and $missing[0] -eq 'BootManager2023') { - return @{ Code='UPDATE_PENDING'; Tag='[~]'; Color='Yellow'; Label='KEK i DB hotové — zbývá aktivovat Boot Manager: vyžaduje RESTART' } + return @{ Code='UPDATE_PENDING'; Tag='[~]'; Color='Yellow' + Label='UEFICA2023Status=Updated, KEK i DB hotové — zbývá aktivovat Boot Manager: vyžaduje RESTART' } + } + if ($anyApplied) { + return @{ Code='UPDATE_PARTIAL'; Tag='[~]'; Color='Cyan' + Label='Probíhá po částech — zbývá: ' + ($missingLabels -join '; ') } } - if ($anyApplied) { return @{ Code='UPDATE_PARTIAL'; Tag='[~]'; Color='Cyan'; Label=('Probíhá po částech — zbývá: ' + ($missingLabels -join '; ')) } } return @{ Code='UPDATE_NEEDED'; Tag='[!]'; Color='Yellow'; Label='Nutná aktualizace certifikátů' } } @@ -506,30 +585,58 @@ function Get-RemediationCategory { function Invoke-Detection { param([bool]$SkipBootMgrFile) + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue - $R = [ordered]@{ - AuditTimestamp=(Get-Date).ToString('yyyy-MM-dd HH:mm:ss'); Hostname=$env:COMPUTERNAME - OSCaption=if($os){$os.Caption}else{$null}; OSBuild=if($os){$os.BuildNumber}else{$null} - 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{'?'} }) - EnvironmentType=Get-EnvironmentType; Hardware=Get-HardwareInfo; SecureBoot=Get-SecureBootState - OperatingMode='Unknown'; BitLocker=$null; Certificates=$null; Registry=Get-RegistryStatus - EventLog=Get-EventLogStatus; TaskExists=Get-TaskExists; BootManager=$null; Phases=$null - Category=$null; CategoryLabel=$null; CategoryColor=$null + $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 + BootManager = $null + Phases = $null + Category = $null + CategoryLabel = $null + CategoryColor = $null + } + if ($R.SecureBoot.IsUEFI -and $R.SecureBoot.IsSupported) { $R.OperatingMode = Get-SecureBootMode $R.Certificates = Get-CertificateStatus $R.BootManager = Get-BootManagerStatus -SkipFileCheck:$SkipBootMgrFile } else { - $R.Certificates = [ordered]@{ KEK=[ordered]@{Has2011=$false;Has2023=$false;Certs2011=@();Certs2023=@();Error='SB nedostupný'} - DB=[ordered]@{Has2011UEFI=$false;Has2011WindowsPCA=$false;Has2023UEFI=$false;Has2023OptionROM=$false;Has2023WindowsUEFI=$false;Certs2011=@();Certs2023=@();Error='SB nedostupný'} - DbxRevokesPCA2011=$false;AnyExpiring2011=$false } - $R.BootManager=[ordered]@{ EspHas2023=$false;Evidence=@();Chain=$null;Signer=$null;Error='SB nedostupný';Checked=$false } + $R.Certificates = [ordered]@{ + KEK = [ordered]@{ Has2011=$false; Has2023=$false; Certs2011=@(); Certs2023=@(); Error='SB nedostupný' } + DB = [ordered]@{ Has2011UEFI=$false; Has2011WindowsPCA=$false; Has2023UEFI=$false + Has2023OptionROM=$false; Has2023WindowsUEFI=$false + Certs2011=@(); Certs2023=@(); Error='SB nedostupný' } + DbxRevokesPCA2011=$false; AnyExpiring2011=$false + } + $R.BootManager = [ordered]@{ EspHas2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error='SB nedostupný'; Checked=$false } } + $R.BitLocker = Get-BitLockerInfo - $R.Phases = Build-Phases -Sb $R.SecureBoot -Cert $R.Certificates -Boot $R.BootManager -TaskExists $R.TaskExists -Reg $R.Registry -Evt $R.EventLog + $R.Phases = Build-Phases -Sb $R.SecureBoot -Cert $R.Certificates -Boot $R.BootManager ` + -TaskExists $R.TaskExists -Reg $R.Registry -Evt $R.EventLog $cat = Get-RemediationCategory -Result $R - $R.Category=$cat.Code; $R.CategoryLabel="$($cat.Tag) $($cat.Label)"; $R.CategoryColor=$cat.Color + $R.Category = $cat.Code + $R.CategoryLabel = "$($cat.Tag) $($cat.Label)" + $R.CategoryColor = $cat.Color return $R } @@ -545,13 +652,17 @@ function Show-DetectionSummary { Write-KV 'Stroj' $R.Hostname 'White' ("· {0} (build {1})" -f $R.OSCaption, $R.OSBuildFull) Write-KV 'Prostředí' $R.EnvironmentType 'White' ("· {0} {1}" -f $hw.Manufacturer, $hw.Model) if ($sb.IsUEFI -and $sb.IsSupported) { - $modeColor = if ($R.OperatingMode -eq 'Setup') {'Red'} elseif ($R.OperatingMode -in @('User','Deployed')) {'White'} else {'Yellow'} - Write-KV 'Secure Boot' ("zapnuto · mode {0}" -f $R.OperatingMode) $(if($sb.IsEnabled){'White'}else{'Yellow'}) $(if($R.OperatingMode -eq 'Setup'){'· Setup Mode = nelze dokončit!'}) + $modeColor = if ($R.OperatingMode -eq 'Setup') { 'Red' } elseif ($R.OperatingMode -in @('User','Deployed')) { 'White' } else { 'Yellow' } + $modeNote = if ($R.OperatingMode -eq 'Setup') { '· Setup Mode = nelze dokončit!' } else { $null } + Write-KV 'Secure Boot' ("zapnuto · mode {0}" -f $R.OperatingMode) $modeColor $modeNote } if ($bl -and $bl.Status -in @('On','Off','1','2','0')) { - $blOn = ($bl.Status -eq 'On' -or $bl.Status -eq '1' -or $bl.Status -eq '2') - $note = if ($blOn -and $bl.UsesPcr) { '· PCR/TPM — před restartem ověř recovery key!' } elseif ($blOn) { '· chráněno' } else { '' } - Write-KV 'BitLocker' ("{0} [{1}]" -f $bl.Status, $bl.Protectors) $(if($blOn -and $bl.UsesPcr){'Yellow'}else{'White'}) $note $(if($blOn -and $bl.UsesPcr){'Yellow'}else{'DarkGray'}) + $blOn = ($bl.Status -eq 'On' -or $bl.Status -eq '1' -or $bl.Status -eq '2') + $blRisk = $blOn -and $bl.UsesPcr + $blNote = if ($blRisk) { '· PCR/TPM — před restartem ověř recovery key!' } elseif ($blOn) { '· chráněno' } else { '' } + $blColor = if ($blRisk) { 'Yellow' } else { 'White' } + $noteColor = if ($blRisk) { 'Yellow' } else { 'DarkGray' } + Write-KV 'BitLocker' ("{0} [{1}]" -f $bl.Status, $bl.Protectors) $blColor $blNote $noteColor } if ($Prereqs) { Show-Prerequisites -Prereqs $Prereqs } @@ -611,42 +722,41 @@ function Show-DetailedDetection { function Resolve-RemediationPlan { param($R, [bool]$ForceMode) - $plan=[ordered]@{ Applicable=$false; AskUser=$false; Caution=$null; Reason=$null; NextSteps=@() } + $plan=[ordered]@{ Applicable=$false; Caution=$null; Reason=$null; NextSteps=@() } switch -Wildcard ($R.Category) { - 'OK' { $plan.Reason='Server je kompletně hotový — žádná akce.'; if ($ForceMode){ $plan.Applicable=$true; $plan.AskUser=$true } } + 'OK' { $plan.Reason='Server je kompletně hotový — žádná akce.'; if ($ForceMode){ $plan.Applicable=$true } } 'OK_TRANSITION' { $plan.Reason='2023 certifikáty i aktivní Boot Manager jsou nasazeny. Staré 2011 zůstávají (normální). Žádná akce.' $plan.NextSteps=@('Nepovinné: časem ověřte revokaci starého boot manageru (DBX).') - if ($ForceMode){ $plan.Applicable=$true; $plan.AskUser=$true } + if ($ForceMode){ $plan.Applicable=$true } } - 'UPDATE_NEEDED' { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Reason='Lze zahájit aktualizaci metodou registry (KB5068202).' } - 'UPDATE_PARTIAL' { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Reason='Část už nasazena, zbytek se aplikuje v dalším cyklu.'; $plan.Caution='Po nastavení a tasku bude potřeba RESTART; opakujte, dokud nebude HOTOVO.' } + 'UPDATE_NEEDED' { $plan.Applicable=$true; $plan.Reason='Lze zahájit aktualizaci metodou registry (KB5068202).' } + 'UPDATE_PARTIAL' { $plan.Applicable=$true; $plan.Reason='Část už nasazena, zbytek se aplikuje v dalším cyklu.'; $plan.Caution='Po nastavení a tasku bude potřeba RESTART; opakujte, dokud nebude HOTOVO.' } 'UPDATE_PENDING' { $au = [int]$R.Registry.AvailableUpdates - # „Staged" = Boot Manager je už připravený k aktivaci (2023 na ESP, nebo AvailableUpdates - # v boot-manager fázi 0x4100/0x4000). Pak STAČÍ RESTART. Jinak je nutné ho teprve naplánovat. - $staged = ([bool]$R.BootManager.EspHas2023) -or ($au -eq 0x4100) -or ($au -eq 0x4000) - if ($staged) { - $plan.Reason='Boot Manager je připravený (staged). Aktivuje se SAMOTNÝM RESTARTEM — víc nastavovat netřeba.' - $plan.NextSteps=@('RESTARTUJTE server (stačí restart, nic dalšího nenastavujte).','BitLocker (PCR7): před restartem ověřte recovery key.','Po restartu spusťte kontrolu — měl by být zelený HOTOVO.') + # 0x4100 = Boot Manager je staged na ESP — stačí RESTART, task nespouštět. + # Vše ostatní (0x4000, 0x0, …) — spustit task, který posune proces dál. + if ($au -eq 0x4100) { + $plan.Reason = 'Boot Manager 2023 je staged (AvailableUpdates=0x4100). Aktivuje se RESTARTEM — task spouštět netřeba.' + $plan.NextSteps = @('RESTARTUJTE server.', 'BitLocker (PCR7): před restartem ověřte recovery key.', 'Po restartu spusťte kontrolu znovu.') } else { - $plan.Applicable=$true; $plan.AskUser=$true - $plan.Reason='KEK i DB hotové, ale Boot Manager ještě NENÍ připravený (AvailableUpdates=0x0, ESP není 2023). Je třeba nastavit registry + spustit task a teprve POTÉ restartovat.' - $plan.Caution='Tímto se naplánuje Boot Manager; aktivuje se až RESTARTEM (re-aplikace už hotových certifikátů je neškodná).' - $plan.NextSteps=@('Po tomto kroku RESTARTUJTE server.','Po restartu spusťte kontrolu znovu (nebo -RegisterResume).') + $plan.Applicable = $true + $plan.Reason = ('KEK i DB hotové, Boot Manager v procesu (AvailableUpdates=0x{0:X}). Task posune proces dál.' -f $au) + $plan.Caution = 'Po dokončení tasku bude nutný RESTART pro aktivaci Boot Manageru.' + $plan.NextSteps = @('Po dokončení tasku RESTARTUJTE server.', 'Po restartu spusťte kontrolu znovu.') } } 'UPDATE_FAILED' { if ($R.EventLog.ById[1795]) { $plan.Reason='Selhání Event 1795 (chyba firmwaru). Příčina je ve firmwaru/hostiteli.' $plan.NextSteps=@('Aktualizujte firmware / Hyper-V hostitele (KB5085790).','Poté spusťte kontrolu znovu.') - if ($ForceMode){ $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Event 1795 = problém firmwaru; -Force jen zopakuje pokus.' } - } else { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Reason='Předchozí pokus selhal. Lze zopakovat.'; $plan.Caution='Zkontrolujte UEFICA2023ErrorEvent výše.' } + if ($ForceMode){ $plan.Applicable=$true; $plan.Caution='Event 1795 = problém firmwaru; -Force jen zopakuje pokus.' } + } else { $plan.Applicable=$true; $plan.Reason='Předchozí pokus selhal. Lze zopakovat.'; $plan.Caution='Zkontrolujte UEFICA2023ErrorEvent výše.' } } 'FIRMWARE_UPDATE_NEEDED' { $plan.Reason='Aktualizace pozastavena (Temporarily Paused / Event 1802) — známý problém firmwaru.' $plan.NextSteps=@('Aktualizujte firmware u výrobce (Dell/HP/Lenovo/Supermicro).','Poté spusťte kontrolu znovu.') - if ($ForceMode){ $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Pozastaveno firmwarem; -Force se pokusí i tak.' } + if ($ForceMode){ $plan.Applicable=$true; $plan.Caution='Pozastaveno firmwarem; -Force se pokusí i tak.' } } 'NOT_SUPPORTED' { $plan.Reason='ConfidenceLevel: Not Supported — zařízení nepodporuje automatickou cestu.' @@ -684,6 +794,37 @@ function Get-UserConsent { } catch { return ((Read-Host 'Aplikovat? [a/N]') -match '^(a|ano|y|yes)$') } } +function Invoke-RestartOffer { + # Nabídne uživateli restart. Skript nikdy nerestartuje automaticky — vždy interaktivní dotaz. + 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 Read-Host)' + } + } +} + #endregion #region ── Stav napříč restarty + RunOnce ───────────────────────────────────── @@ -715,33 +856,30 @@ function Register-Resume { function Invoke-Remediation { param([bool]$SkipBootMgrFile) - $out=[ordered]@{ Status='Unknown'; Message=''; After=$null; LogFile=$script:LogFile } + $out=[ordered]@{ Status='Unknown'; Message=''; After=$null } Write-Head 'REMEDIACE'; Add-LogLine 'REMEDIACE START' - # Přečti AvailableUpdates ještě PŘED zápisem — rozhodnutí, jestli ho nastavit nebo zachovat. - $auBefore = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue).AvailableUpdates - # "In progress" = nenulová hodnota → systém ji spravuje sám (dle KB5068202 nastavit jen jednou). - # Null nebo 0 → první start nebo re-trigger (Boot Manager není aktivní přesto, že certifikáty jsou OK). - $auInProgress = ($null -ne $auBefore) -and ([int]$auBefore -ne 0) - $auLabel = if ($auInProgress) { '0x{0:X} zachováno (probíhá)' -f [int]$auBefore } else { '0x5944 (nové/reset)' } - Write-Host (" [1/3] Nastavuji registry (MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}) ... " -f $auLabel) -NoNewline + # AvailableUpdates se nastavuje na 0x5944 POUZE jednou — při prvním spuštění (null) nebo po + # dokončení předchozího cyklu (0). Systém si pak hodnotu sám spravuje: odečítá bity jak postupuje + # (0x5944→0x5904→0x5104→0x4104→0x4100→0x4000→0x0). Skript ji neresetuje — jen čte a interpretuje. + $auCurrent = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue).AvailableUpdates + $auNeedsInit = ($null -eq $auCurrent) -or ([int]$auCurrent -eq 0) + $auInfo = if ($auNeedsInit) { '0x5944 (první inicializace)' } else { '0x{0:X} zachováno (probíhá)' -f [int]$auCurrent } + Write-Host (" [1/3] Nastavuji registry (MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}) ... " -f $auInfo) -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 - # AvailableUpdates nastavujeme na 0x5944 POUZE pokud ještě nebyl nastaven (null) nebo je 0. - # Pokud je nenulový, hodnotu zachováme — resetování by smazalo již dosažený postup. - if (-not $auInProgress) { + if ($auNeedsInit) { Set-ItemProperty -Path $REG_SECUREBOOT -Name 'AvailableUpdates' -Value $AVAILABLE_UPDATES_VALUE -Type DWord -Force -ErrorAction Stop + $vCheck = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction Stop).AvailableUpdates + if ([int]$vCheck -ne $AVAILABLE_UPDATES_VALUE) { throw ('Ověření selhalo — AvailableUpdates=0x{0:X}, očekáváno 0x5944' -f [int]$vCheck) } } $o = Get-ItemProperty $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -ErrorAction SilentlyContinue if ($o -and $o.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 } - $vNow = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction Stop).AvailableUpdates - $vExpected = if ($auInProgress) { [int]$auBefore } else { $AVAILABLE_UPDATES_VALUE } - if ([int]$vNow -ne $vExpected) { throw ('Ověření selhalo — AvailableUpdates=0x{0:X}, očekáváno 0x{1:X}' -f [int]$vNow, $vExpected) } Write-Host 'hotovo' -ForegroundColor Green - $auAction = if ($auInProgress) { 'zachováno' } else { 'nastaveno' } - Add-LogLine ('Krok 1: MicrosoftUpdateManagedOptIn=1, AvailableUpdates=0x{0:X} ({1})' -f [int]$vNow, $auAction) + $auLogVal = if ($auNeedsInit) { '0x5944 (nastaveno)' } else { '0x{0:X} (zachováno)' -f [int]$auCurrent } + Add-LogLine ("Krok 1: MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}" -f $auLogVal) } catch { Write-Host 'CHYBA' -ForegroundColor Red; Write-Line (" {0}" -f $_.Exception.Message) Red; $out.Status='Error'; $out.Message="Zápis registry selhal: $($_.Exception.Message)"; return $out } if ($SkipScheduledTask) { Write-Line ' [2/3] Servicing task přeskočen (-SkipScheduledTask).' DarkGray } @@ -773,7 +911,16 @@ function Invoke-Remediation { $after=Invoke-Detection -SkipBootMgrFile:$SkipBootMgrFile Write-Host 'hotovo' -ForegroundColor Green; Write-Host '' Write-Line (' AvailableUpdates: {0}' -f (Get-AvailableUpdatesText $after.Registry.AvailableUpdates)) Cyan - foreach ($k in @('Kek2023','Db2023Windows','BootManager2023')) { $p=$after.Phases[$k]; Write-Check -State $p.State -Text $p.Label -Note $p.Note } + Write-Line (' UEFICA2023Status : {0}' -f $after.Registry.UEFICA2023StatusText) $(if($after.Registry.UEFICA2023StatusText -eq 'Updated'){'Green'}else{'White'}) + foreach ($k in @('Kek2023','Db2023Windows','CertServicingStatus','BootManager2023')) { $p=$after.Phases[$k]; Write-Check -State $p.State -Text $p.Label -Note $p.Note } + $afterAU = [int]$after.Registry.AvailableUpdates + if ($afterAU -eq 0x4100) { + Write-Host '' + Write-Host ' *** Boot Manager 2023 je staged (AvailableUpdates=0x4100) ***' -ForegroundColor Yellow + Write-Host ' Aktivace proběhne po restartu serveru.' -ForegroundColor Yellow + Add-LogLine 'Post-remediace: AU=0x4100 — BM staged, vyžaduje restart' + Invoke-RestartOffer + } $out.Status='Applied'; $out.After=$after; $out.Message='Registry nastavena, task zpracován. Boot Manager se aktivuje až po restartu.' Add-LogLine 'REMEDIACE KONEC: Applied' return $out @@ -791,8 +938,8 @@ function Get-StageInfo { 0x5904 { return @{ Step=2; Total=6; Label='Windows UEFI CA 2023 v DB' } } 0x5104 { return @{ Step=3; Total=6; Label='Option ROM UEFI CA 2023 v DB' } } 0x4104 { return @{ Step=4; Total=6; Label='Microsoft UEFI CA 2023 v DB' } } - 0x4100 { return @{ Step=5; Total=6; Label='Boot Manager nasazen na ESP' } } - 0x4000 { return @{ Step=6; Total=6; Label='čeká na finální restart' } } + 0x4100 { return @{ Step=5; Total=6; Label='Boot Manager nasazen na ESP — RESTART' } } + 0x4000 { return @{ Step=6; Total=6; Label='guard bit — task dokončuje' } } 0 { return @{ Step=6; Total=6; Label='dokončeno' } } default { return @{ Step=1; Total=6; Label=('mezistav 0x{0:X}' -f [int]$v) } } } @@ -821,21 +968,52 @@ function Show-Progress { function Get-Prerequisites { param($R, [bool]$IsAdmin) - $sb = $R.SecureBoot + $sb = $R.SecureBoot $list = @() - $list += @{ Label='Spuštěno jako Administrator'; Ok=$IsAdmin; Hard=$true; Note=$(if(-not $IsAdmin){'spusťte PowerShell „Run as administrator"'}) } - $list += @{ Label='UEFI + Secure Boot podporováno'; Ok=($sb.IsUEFI -and $sb.IsSupported); Hard=$true; Note=$(if(-not ($sb.IsUEFI -and $sb.IsSupported)){'Legacy BIOS / nepodporováno'}) } - $list += @{ Label='Secure Boot zapnutý'; Ok=[bool]$sb.IsEnabled; Hard=$true; Note=$(if($sb.IsUEFI -and $sb.IsSupported -and -not $sb.IsEnabled){'zapněte v UEFI'}) } - $list += @{ Label='Ne Setup Mode (enrolled PK)'; Ok=($R.OperatingMode -ne 'Setup'); Hard=$true; Note=$(if($R.OperatingMode -eq 'Setup'){'obnovte Secure Boot klíče v UEFI'}) } - $buildNote = if ($R.TaskExists) { "build $($R.OSBuildFull)" } else { "build $($R.OSBuildFull) — chybí task, nainstalujte 10/2025+ (KB5066835)" } - $list += @{ Label='Build s podporou aktualizace (servicing task)'; Ok=[bool]$R.TaskExists; Hard=$true; Note=$buildNote } - # Boot-manager servicing přítomný? Když jsou certy v DB/KEK, ale WindowsUEFICA2023Capable i - # UEFICA2023Status chybí a boot manager není aktivní → build je příliš starý (nutný Windows Update). - $auP = [int]$R.Registry.AvailableUpdates + + $list += @{ Label='Spuštěno jako Administrator' + Ok=$IsAdmin; Hard=$true + Note=if(-not $IsAdmin){'spusťte PowerShell „Run as administrator"'} } + + $sbOk = $sb.IsUEFI -and $sb.IsSupported + $list += @{ Label='UEFI + Secure Boot podporováno' + Ok=$sbOk; Hard=$true + Note=if(-not $sbOk){'Legacy BIOS / nepodporováno'} } + + $list += @{ Label='Secure Boot zapnutý' + Ok=[bool]$sb.IsEnabled; Hard=$true + Note=if($sbOk -and -not $sb.IsEnabled){'zapněte v UEFI'} } + + $list += @{ Label='Ne Setup Mode (enrolled PK)' + Ok=($R.OperatingMode -ne 'Setup'); Hard=$true + Note=if($R.OperatingMode -eq 'Setup'){'obnovte Secure Boot klíče v UEFI'} } + + $buildNote = if ($R.TaskExists) { + "build $($R.OSBuildFull)" + } else { + "build $($R.OSBuildFull) — chybí task, nainstalujte 10/2025+ (KB5066835)" + } + $list += @{ Label='Build s podporou aktualizace (servicing task)' + Ok=[bool]$R.TaskExists; Hard=$true; Note=$buildNote } + + # Starý build: certy jsou v KEK/DB, ale WindowsUEFICA2023Capable + UEFICA2023Status zcela chybí + # a BM není staged → servicing infrastruktura pro Boot Manager neexistuje, nutný Windows Update. + $auP = [int]$R.Registry.AvailableUpdates $bmStagedP = ([bool]$R.BootManager.EspHas2023) -or ($auP -eq 0x4100) -or ($auP -eq 0x4000) - $staleServ = ($R.Phases['Kek2023'].Done -or $R.Phases['Db2023Windows'].Done) -and (-not $R.Phases['BootManager2023'].Done) -and (-not $bmStagedP) -and ($null -eq $R.Registry.WindowsUEFICA2023Capable) -and ($null -eq $R.Registry.UEFICA2023Status) - $list += @{ Label='Servicing pro Boot Manager 2023 (aktuální build)'; Ok=(-not $staleServ); Hard=$true; Note=$(if($staleServ){"build $($R.OSBuildFull) je starý — chybí 2023 boot-manager servicing, nutný Windows Update"}) } - $list += @{ Label='Zařízení není „Not Supported"'; Ok=($R.Registry.ConfidenceLevel -notlike 'Not Supported*'); Hard=$false; Note=$(if($R.Registry.ConfidenceLevel -like 'Not Supported*'){'ConfidenceLevel: Not Supported — řešení u OEM'}) } + $staleServ = ($R.Phases['Kek2023'].Done -or $R.Phases['Db2023Windows'].Done) ` + -and (-not $R.Phases['BootManager2023'].Done) ` + -and (-not $bmStagedP) ` + -and ($null -eq $R.Registry.WindowsUEFICA2023Capable) ` + -and ($null -eq $R.Registry.UEFICA2023Status) + $list += @{ Label='Servicing pro Boot Manager 2023 (aktuální build)' + Ok=(-not $staleServ); Hard=$true + Note=if($staleServ){"build $($R.OSBuildFull) je starý — chybí 2023 boot-manager servicing, nutný Windows Update"} } + + $notSupported = $R.Registry.ConfidenceLevel -like 'Not Supported*' + $list += @{ Label='Zařízení není „Not Supported"' + Ok=(-not $notSupported); Hard=$false + Note=if($notSupported){'ConfidenceLevel: Not Supported — řešení u OEM'} } + return $list } @@ -901,6 +1079,10 @@ $remediation = $null if (-not $plan.Applicable) { if ($plan.NextSteps.Count) { Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White; $i=1; foreach ($s in $plan.NextSteps) { Write-Host (" {0}. {1}" -f $i,$s) -ForegroundColor Gray; $i++ } } elseif ($result.Category -like 'OK*') { Write-Host ' Není potřeba žádná akce.' -ForegroundColor Green } + # UPDATE_PENDING na 0x4100: Boot Manager staged — nabídnout restart přímo z detekce + if ($result.Category -eq 'UPDATE_PENDING' -and -not $CheckOnly -and -not $isWhatIf -and $isAdmin) { + if ([int]$result.Registry.AvailableUpdates -eq 0x4100) { Invoke-RestartOffer } + } } else { if ($plan.Caution) { Write-Host (" Pozor: {0}" -f $plan.Caution) -ForegroundColor Yellow } if (-not $isAdmin) { Write-Host ''; Write-Host ' Remediaci nelze provést bez práv Administrator.' -ForegroundColor Yellow } @@ -928,7 +1110,7 @@ if ($remediation -and $remediation.Status -eq 'Applied') { else { Write-Host ' ČÁSTEČNĚ HOTOVO — proces pokračuje po restartu.' -ForegroundColor Yellow Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White - Write-Host ' 1. Naplánujte RESTART serveru (skript jej záměrně neprovedl).' -ForegroundColor Gray + Write-Host ' 1. RESTART serveru (byl nabídnut výše; pokud jste odmítli, restartujte ručně).' -ForegroundColor Gray if ($after.BitLocker -and $after.BitLocker.UsesPcr -and ($after.BitLocker.Status -eq 'On' -or $after.BitLocker.Status -eq '1' -or $after.BitLocker.Status -eq '2')) { Write-Host ' 2. BitLocker (PCR/TPM) je aktivní — před restartem ověřte recovery key!' -ForegroundColor Yellow } else { Write-Host ' 2. BitLocker s PCR7: před restartem ověřte recovery key.' -ForegroundColor Gray }