diff --git a/Invoke-SecureBootRemediation.ps1 b/Invoke-SecureBootRemediation.ps1 index a5c4bfe..c2450a4 100644 --- a/Invoke-SecureBootRemediation.ps1 +++ b/Invoke-SecureBootRemediation.ps1 @@ -1,59 +1,44 @@ #Requires -Version 5.1 <# .SYNOPSIS - Detekce + interaktivní remediace Secure Boot certifikátů na jednom serveru (KB5062710 / KB5068202). + Detekce a interaktivní remediace Secure Boot certifikátů (KB5062710 / KB5068202). .DESCRIPTION - Sloučení detekčního a remediačního skriptu do jednoho lokálního průvodce s fázovým - checklistem. Server prohlásí za HOTOVÝ teprve když jsou splněné VŠECHNY povinné fáze — - včetně toho, že se systém reálně bootuje z Boot Manageru podepsaného Windows UEFI CA 2023 - (signalizuje WindowsUEFICA2023Capable=2 / Event 1808 / 1799). + Script zkontroluje přítomnost nových 2023 Secure Boot certifikátů, zobrazí stav + ve čtyřech sekcích a nabídne remediaci. - Respektuje oficiální MS guidance (autoritativní hodnoty): - - UEFICA2023Status (REG_SZ): NotStarted / InProgress / Updated - - WindowsUEFICA2023Capable: 0=cert není v DB, 1=cert v DB, 2=bootuje se z 2023 boot manageru - - 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): 1795=chyba firmwaru, 1796=selhání zápisu UEFI var, - 1802=pozastaveno (firmware), 1803=odloženo (monitor), 1801=čeká (monitor) + 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. - 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). + 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 - Jen detekce + checklist + exit kód (0=hotovo, 1=nutná akce, 2=blokováno). Bez změn/dotazu. + Pouze zobrazí stav bez dotazů a změn. + Exit kódy: 0 = hotovo, 1 = nutná akce, 2 = blokováno. .PARAMETER AssumeYes - Přeskočí dotaz a remediaci rovnou aplikuje (pokud je smysluplná). Bez restartu. + Přeskočí interaktivní dotaz a remediaci rovnou aplikuje (pokud je smysluplná). .PARAMETER Force - Aplikuje remediaci i ve stavech, kdy ji skript jinak nedoporučuje. + Spustí remediaci i v případě, že povinné certifikáty jsou již přítomny. -.PARAMETER SkipScheduledTask - 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 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 - automaticky spustí kontrolu (-CheckOnly). Nikdy nerestartuje sám. - -.PARAMETER Detailed - Rozšířený rozpis (certifikáty, události, boot manager chain, bit-breakdown AvailableUpdates). - -.PARAMETER PassThru - Vrátí výsledný objekt do pipeline. - -.PARAMETER LogPath - Cesta k logu. Default: log vedle skriptu vznikne jen při reálné remediaci. +.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 (čtení UEFI databází, mount ESP, zápis registry). + Vyžaduje Administrator pro zápis registry a spuštění servicing task. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] @@ -61,20 +46,15 @@ param( [switch]$CheckOnly, [switch]$AssumeYes, [switch]$Force, - [switch]$SkipScheduledTask, - [switch]$SkipBootManagerFileCheck, - [switch]$RegisterResume, - [switch]$Detailed, - [switch]$PassThru, - [string]$LogPath + [switch]$AutoRestart ) $ErrorActionPreference = 'SilentlyContinue' Set-StrictMode -Off -# ── Konzole: vynutit UTF-8 výstup (diakritika + symboly i ve Windows PowerShell 5.1) ── +# Vynutit UTF-8 výstup (diakritika i ve Windows PowerShell 5.1) try { - $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [Console]::OutputEncoding = $utf8NoBom $OutputEncoding = $utf8NoBom } catch { } @@ -83,7 +63,6 @@ try { $REG_SECUREBOOT = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot' $REG_SERVICING = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' -$REG_RUNONCE = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' $AVAILABLE_UPDATES_VALUE = 0x5944 $TASK_PATH = '\Microsoft\Windows\PI\' $TASK_NAME = 'Secure-Boot-Update' @@ -97,141 +76,188 @@ $CN_UEFI2023 = 'Microsoft UEFI CA 2023' $CN_OPTROM2023 = 'Option ROM UEFI CA 2023' $CN_PCA2011 = 'Windows Production PCA 2011' -# Bity AvailableUpdates (dle CheckCA2023 / MS) — pro -Detailed rozpis -$AU_BITS = @( - @{ Bit=0x0002; Name='DBX revocation update' } - @{ Bit=0x0004; Name='KEK 2K CA 2023 -> KEK' } - @{ Bit=0x0020; Name='SkuSiPolicy update' } - @{ Bit=0x0040; Name='Windows UEFI CA 2023 -> DB' } - @{ Bit=0x0080; Name='PCA 2011 -> DBX (revoke old boot mgr)' } - @{ Bit=0x0100; Name='Boot manager update' } - @{ Bit=0x0200; Name='SVN firmware update' } - @{ Bit=0x0400; Name='SBAT firmware update' } - @{ Bit=0x0800; Name='Option ROM UEFI CA 2023 -> DB' } - @{ Bit=0x1000; Name='Microsoft UEFI CA 2023 -> DB' } - @{ Bit=0x4000; Name='Conditional CA 2023 guard bit' } -) - -# Značky checklistu. ASCII kvůli kompatibilitě — font Consolas (default konzole) nemá blok -# Dingbats, takže ✓/✗ by se zobrazily jako □. Chcete-li v Consolas „fajfku", nastavte -# $SYM_DONE = [string][char]0x221A (√, blok Mathematical Operators, Consolas ho má). $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 ─────────────────────────────────────────────── -# 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é. -$script:LogFile = if ($LogPath) { $LogPath } else { Join-Path $WORK_ROOT 'SecureBootRemediation.log' } +$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))) + [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-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,-16}: " -f $Key) -ForegroundColor Gray -NoNewline + 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 '' } + 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') + 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="[$SYM_PENDING]"; $mc='DarkGray'; $tc='Gray' } - 'Info' { $mark="[$SYM_PENDING]"; $mc='DarkGray'; $tc='DarkGray' } + '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 '' } + 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 / mode / BitLocker ────────── +#region ── Detekce — prostředí / HW / Secure Boot ──────────────────────────── function Get-EnvironmentType { try { - $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop - $mfr=[string]$cs.Manufacturer; $model=[string]$cs.Model - if ($mfr -like '*VMware*') { return 'VMware VM' } - if ($mfr -like '*Microsoft*' -and $model -eq 'Virtual Machine') { return 'Hyper-V VM' } - if ($model -like '*Virtual*' -or $mfr -like '*QEMU*' -or $mfr -like '*Xen*') { return 'Other VM' } + $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 { } + $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') } + $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' } + 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 } + $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 } + 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 { - # User / Deployed / Setup / Audit ze standardních UEFI proměnných - $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 { } + $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' } + 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 } + $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.Status = [string]$v.ProtectionStatus + $types = @($v.KeyProtector | ForEach-Object { [string]$_.KeyProtectorType }) $i.Protectors = ($types -join ', ') - $i.UsesPcr = [bool](@($types | Where-Object { $_ -like 'Tpm*' }).Count) + $i.UsesPcr = [bool](@($types | Where-Object { $_ -like 'Tpm*' }).Count) } } catch { $i.Status = 'N/A' } return $i @@ -239,16 +265,17 @@ function Get-BitLockerInfo { #endregion -#region ── Detekce — certifikáty / registry / events / boot manager ─────────── +#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, v každé seznam SignatureEntry s 16B GUID prefixem. + # 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 } - # GUID pro EFI_CERT_X509_GUID: {a5c059a1-94e4-4aa7-87b5-ab155c2bf072} (little-endian bytes) + # 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 @@ -260,15 +287,16 @@ function Parse-EFISignatureList { 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 } } + 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 + $certOffset = $entryPos + 16 # přeskočit 16B SignatureOwner GUID $certSize = [int]$sigSize - 16 if ($certOffset + $certSize -le $Bytes.Length -and $certSize -gt 0) { try { @@ -283,115 +311,187 @@ function Parse-EFISignatureList { } return $certs } + function Convert-CertToInfo { param($Cert) - [ordered]@{ Subject=$Cert.Subject; Thumbprint=$Cert.Thumbprint; NotAfter=$Cert.NotAfter.ToString('yyyy-MM-dd') } + [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 } + 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 + 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 } + 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 } + } 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 } + 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 - $k=Get-SbVarAscii KEK; if ($k -and $k -match [regex]::Escape($CN_KEK2023)) { $st.KEK.Has2023=$true } - $d=Get-SbVarAscii db - if ($d) { - if ($d -match [regex]::Escape($CN_WINUEFI2023)) { $st.DB.Has2023WindowsUEFI=$true } - if ($d -match [regex]::Escape($CN_OPTROM2023)) { $st.DB.Has2023OptionROM=$true } - if ($d -match 'Microsoft UEFI CA 2023') { $st.DB.Has2023UEFI=$true } + } 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 } } - $x=Get-SbVarAscii dbx; if ($x -and $x -match [regex]::Escape($CN_PCA2011)) { $st.DbxRevokesPCA2011=$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 + AvailableUpdates = $null + HighConfidenceOptOut = $null + MicrosoftUpdateManagedOptIn = $null + ServicingKeyExists = $false + UEFICA2023Status = $null + UEFICA2023StatusText = 'KeyNotPresent' + UEFICA2023Error = $null + UEFICA2023ErrorEvent = $null + WindowsUEFICA2023Capable = $null + WindowsUEFICA2023CapableText = '-' + ConfidenceLevel = $null } - $m = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue - if ($m) { - $reg.AvailableUpdates=$m.AvailableUpdates; $reg.HighConfidenceOptOut=$m.HighConfidenceOptOut - $reg.MicrosoftUpdateManagedOptIn=$m.MicrosoftUpdateManagedOptIn + + $main = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue + if ($main) { + $reg.AvailableUpdates = $main.AvailableUpdates + $reg.HighConfidenceOptOut = $main.HighConfidenceOptOut + $reg.MicrosoftUpdateManagedOptIn = $main.MicrosoftUpdateManagedOptIn } - $s = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue - if ($s) { - $reg.ServicingKeyExists=$true - $reg.UEFICA2023Status=$s.UEFICA2023Status - $reg.UEFICA2023Error=$s.UEFICA2023Error - $reg.UEFICA2023ErrorEvent=$s.UEFICA2023ErrorEvent - $reg.WindowsUEFICA2023Capable=$s.WindowsUEFICA2023Capable - $reg.ConfidenceLevel=$s.ConfidenceLevel + + $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); defenzivně i číselná varianta - $v=$reg.UEFICA2023Status - if ($null -eq $v) { $reg.UEFICA2023StatusText='KeyNotPresent' } - elseif ($v -is [string] -and $v -ne '') { $reg.UEFICA2023StatusText=$v } - else { $reg.UEFICA2023StatusText = switch ([int]$v) { 0{'NotStarted'} 1{'InProgress'} 2{'Updated'} 3{'Failed'} default {"($v)"} } } - # WindowsUEFICA2023Capable: 0/1/2 + + # 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 = cert v DB (boot mgr zatím ne)'} - 2 {'2 = bootuje se z 2023 boot manageru'} $null {'-'} default {[string]$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 - $n = 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á 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)" + $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) + $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 - # 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') } } + 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)) { + foreach ($ev in ($sorted | Select-Object -First 10)) { $e.RelevantEvents += [ordered]@{ EventId = $ev.Id TimeCreated = $ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') @@ -403,180 +503,132 @@ function Get-EventLogStatus { $e.LastEventTime = $last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') } } catch { - if ($_.CategoryInfo.Reason -ne 'NoMatchingEventsException') { $e.Error=$_.Exception.Message } + 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 } -} - -function Get-BootManagerStatus { - # Ověří, zda je bootmgfw.efi na ESP podepsaný Windows UEFI CA 2023 (staged stav). - param([bool]$SkipFileCheck) - $r=[ordered]@{ EspHas2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error=$null; Checked=$false } - if ($SkipFileCheck) { return $r } - $drive='S'; $mounted=$false; $tmp=$null try { - if (-not (Get-PSDrive -Name $drive -ErrorAction SilentlyContinue)) { & mountvol "$drive`:" /S 2>$null | Out-Null; Start-Sleep -Seconds 2; $mounted=$true } - $src="$drive`:\EFI\Microsoft\Boot\bootmgfw.efi" - if (Test-Path -LiteralPath $src) { - $r.Checked=$true - $tmp=Join-Path $env:TEMP ("bootmgfw_{0}.efi" -f ([guid]::NewGuid().ToString('N'))) - Copy-Item -LiteralPath $src -Destination $tmp -Force -ErrorAction Stop - if (Get-Command certutil.exe -ErrorAction SilentlyContinue) { - $raw = & certutil.exe -dump $tmp 2>&1 | Out-String - if ($raw -match 'Windows UEFI CA 2023') { $r.EspHas2023=$true; $r.Evidence+='certutil' } - $iss=([regex]::Match($raw,'(?im)^\s*Issuer:\s*(.+)$')).Groups[1].Value.Trim(); if ($iss){ $r.Chain="Issuer: $iss" } - } - try { $sig=Get-AuthenticodeSignature -FilePath $tmp -ErrorAction Stop; if ($sig.SignerCertificate){ $r.Signer=$sig.SignerCertificate.Subject } } catch { } - } else { $r.Error='bootmgfw.efi nenalezen na ESP' } - } catch { $r.Error=$_.Exception.Message } - finally { - if ($tmp -and (Test-Path -LiteralPath $tmp)) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } - if ($mounted) { & mountvol "$drive`:" /D 2>$null | Out-Null } - } - return $r + $null = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop + return $true + } catch { return $false } } #endregion -#region ── Fáze + kategorizace ──────────────────────────────────────────────── +#region ── Kategorizace ─────────────────────────────────────────────────────── -function Build-Phases { - param($Sb, $Cert, $Boot, $TaskExists, $Reg, $Evt) - $e1795=$Evt.ById[1795]; $e1796=$Evt.ById[1796] - $cap=$Reg.WindowsUEFICA2023Capable - # 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['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 - foreach ($key in $ph.Keys) { - $p=$ph[$key] - if ($p.Done) { $p.State='Done'; continue } - if (-not $p.Req) { $p.State='Info'; continue } - switch ($key) { - 'Kek2023' { - if ($e1796) { $p.State='Fail'; $p.Note='selhání zápisu proměnné (Event 1796)' } - 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 -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' } - } - } - } - return $ph +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 - $env=$Result.EnvironmentType; $ph=$Result.Phases; $cert=$Result.Certificates; $mode=$Result.OperatingMode + $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) { - $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 ($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'; Tag='[OFF]'; Color='Yellow'; Label='Secure Boot vypnuto' } + return @{ Code='SECUREBOOT_DISABLED'; Color='Yellow' + Label='Secure Boot je vypnutý' } } if ($mode -eq 'Setup') { - return @{ Code='SETUP_MODE'; Tag='[!]'; Color='Magenta'; Label='Secure Boot v Setup Mode — aktualizaci nelze dokončit (chybí enrolled PK)' } + return @{ Code='SETUP_MODE'; 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 $Result.TaskExists) { + return @{ Code='TASK_MISSING'; Color='Magenta' + Label='Chybí servicing task — nainstalujte kumulativní update min. z 10/2025 (KB5066835)' } } - # ── 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)' } + # ── 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'; Tag='[X]'; Color='Red'; Label='Zařízení nepodporuje automatickou aktualizaci (ConfidenceLevel: 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'; Tag='[FW]'; Color='Magenta'; Label='Aktualizace pozastavena (známý problém) — zkontrolujte firmware u OEM' } + return @{ Code='FIRMWARE_UPDATE_NEEDED'; Color='Magenta' + Label='Aktualizace pozastavena — problém firmwaru, zkontrolujte update u OEM' } } - # ── 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)) { - return @{ Code='BUILD_OUTDATED'; Tag='[!]'; Color='Magenta'; Label='Build je příliš starý — chybí servicing pro Boot Manager 2023. Nutný Windows Update.' } - } - - # ── 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) { + # ── Hotovo (všechny 4 post-remediation podmínky) ────────────────────────── + if (Test-RemediationComplete -R $Result) { 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_TRANSITION'; Color='Green' + Label='HOTOVO — 2023 certifikáty nasazeny (staré 2011 ještě přítomné, je to normální)' } } - return @{ Code='OK'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — kompletní 2023 sada, aktivní Boot Manager, 2011 odstraněny' } + return @{ Code='OK'; Color='Green' + Label='HOTOVO — 2023 certifikáty nasazeny, servisování dokončeno' } } - if ($missing.Count -eq 1 -and $missing[0] -eq 'BootManager2023') { - return @{ Code='UPDATE_PENDING'; Tag='[~]'; Color='Yellow' - Label='UEFICA2023Status=Updated, KEK i DB hotové — zbývá aktivovat Boot Manager: vyžaduje RESTART' } + + # ── 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' } } - if ($anyApplied) { - return @{ Code='UPDATE_PARTIAL'; Tag='[~]'; Color='Cyan' - Label='Probíhá po částech — zbývá: ' + ($missingLabels -join '; ') } + + $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' } } - return @{ Code='UPDATE_NEEDED'; Tag='[!]'; Color='Yellow'; Label='Nutná aktualizace certifikátů' } + + # ── 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 @@ -584,8 +636,6 @@ function Get-RemediationCategory { #region ── Detekce — orchestrace ────────────────────────────────────────────── function Invoke-Detection { - param([bool]$SkipBootMgrFile) - $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $osBuildFull = try { $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop @@ -608,8 +658,6 @@ function Invoke-Detection { Registry = Get-RegistryStatus EventLog = Get-EventLogStatus TaskExists = Get-TaskExists - BootManager = $null - Phases = $null Category = $null CategoryLabel = $null CategoryColor = $null @@ -618,184 +666,214 @@ function Invoke-Detection { 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 + 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.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 - $cat = Get-RemediationCategory -Result $R + + $cat = Get-RemediationCategory -Result $R $R.Category = $cat.Code - $R.CategoryLabel = "$($cat.Tag) $($cat.Label)" + $R.CategoryLabel = $cat.Label $R.CategoryColor = $cat.Color + return $R } #endregion -#region ── Výpis ────────────────────────────────────────────────────────────── +#region ── Zobrazení ────────────────────────────────────────────────────────── -function Show-DetectionSummary { - param($R, $Prereqs, $Prev) - $sb=$R.SecureBoot; $c=$R.Certificates; $reg=$R.Registry; $evt=$R.EventLog; $hw=$R.Hardware; $ph=$R.Phases; $bl=$R.BitLocker - - Write-Head 'SERVER' - 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' } - $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 +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++ } - 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') - $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 + $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 } - - if ($Prereqs) { Show-Prerequisites -Prereqs $Prereqs } - - Write-Head 'POSTUP AKTUALIZACE (checklist)' - if ($sb.IsUEFI -and $sb.IsSupported) { foreach ($k in $ph.Keys) { $p=$ph[$k]; Write-Check -State $p.State -Text $p.Label -Note $p.Note } } - else { Write-Check -State 'Fail' -Text 'Secure Boot není podporováno / zapnuto' -Note 'remediace zde nedává smysl' } - - Show-Progress -R $R -Prev $Prev - - Write-Head 'STAV REGISTRŮ / FIRMWARE' - Write-KV 'AvailableUpdates' (Get-AvailableUpdatesText $reg.AvailableUpdates) 'Cyan' - Write-KV 'UEFICA2023Status' $reg.UEFICA2023StatusText $(if($reg.UEFICA2023StatusText -eq 'Updated'){'Green'}elseif($reg.UEFICA2023StatusText -eq 'Failed'){'Red'}else{'White'}) - Write-KV 'WinUEFICA2023Capable' $reg.WindowsUEFICA2023CapableText $(if($reg.WindowsUEFICA2023Capable -eq 2){'Green'}else{'White'}) - if ($reg.ConfidenceLevel) { Write-KV 'ConfidenceLevel' $reg.ConfidenceLevel $(if($reg.ConfidenceLevel -like 'High*'){'Green'}elseif($reg.ConfidenceLevel -like 'Not Supported*'){'Red'}else{'Yellow'}) } - if ($null -ne $reg.UEFICA2023ErrorEvent -and [int]$reg.UEFICA2023ErrorEvent -ne 0) { Write-KV 'UEFICA2023ErrorEvent' ([string]$reg.UEFICA2023ErrorEvent) 'Red' '· Event ID poslední chyby' } - if ($R.BootManager.Error -and -not $SkipBootManagerFileCheck) { Write-KV 'Boot Manager (ESP)' 'neověřeno souborově' 'Yellow' ("· {0}" -f $R.BootManager.Error) } - elseif ($R.BootManager.EspHas2023) { Write-KV 'Boot Manager (ESP)' 'podepsán CA 2023 (staged)' 'Green' } - if ($evt.LastEventId) { Write-KV 'Poslední event' ("EventID {0}" -f $evt.LastEventId) $(if($evt.LastEventId -in @(1808,1799)){'Green'}elseif($evt.LastEventId -in @(1795,1796)){'Red'}else{'White'}) ("· {0}" -f $evt.LastEventTime) } - Write-Host ' cílový stav: AvailableUpdates=0x0 · UEFICA2023Status=Updated · WinUEFICA2023Capable=2' -ForegroundColor DarkGray - - Write-Host '' - Write-Host ' VÝSLEDEK: ' -ForegroundColor Gray -NoNewline - Write-Host $R.CategoryLabel -ForegroundColor $R.CategoryColor - Add-LogLine ("VÝSLEDEK: {0}" -f $R.CategoryLabel) } -function Show-DetailedDetection { +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) - $c=$R.Certificates; $evt=$R.EventLog; $bm=$R.BootManager; $reg=$R.Registry - Write-Head 'PODROBNOSTI' - foreach ($g in @(@{N='KEK';O=$c.KEK}, @{N='DB';O=$c.DB})) { - foreach ($ci in (@($g.O.Certs2011)+@($g.O.Certs2023))) { - Write-Host (" [{0}] {1}" -f $g.N, $ci.Subject) -ForegroundColor DarkGray - Write-Host (" do {0} | {1}" -f $ci.NotAfter, $ci.Thumbprint) -ForegroundColor DarkGray - } - } - if ($bm.Chain) { Write-Host (" Boot Manager chain: {0}" -f $bm.Chain) -ForegroundColor DarkGray } - if ($bm.Signer) { Write-Host (" Boot Manager signer: {0}" -f $bm.Signer) -ForegroundColor DarkGray } - if ($null -ne $reg.AvailableUpdates) { - Write-Host ' AvailableUpdates — naplánované operace (bit):' -ForegroundColor DarkGray - $iv=[int]$reg.AvailableUpdates - foreach ($b in $AU_BITS) { if ($iv -band $b.Bit) { Write-Host (" 0x{0:X4} {1}" -f $b.Bit, $b.Name) -ForegroundColor Yellow } } - } - if ($evt.RelevantEvents.Count) { - Write-Host ' Poslední události:' -ForegroundColor DarkGray - foreach ($e in ($evt.RelevantEvents|Select-Object -First 6)) { - $ec=if($e.EventId -in @(1808,1799)){'Green'}elseif($e.EventId -in @(1795,1796)){'Red'}else{'DarkGray'} - Write-Host (" [{0}] EventID {1} {2}" -f $e.TimeCreated, $e.EventId, $e.Level) -ForegroundColor $ec - } + $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 ── Plán remediace + souhlas ─────────────────────────────────────────── - -function Resolve-RemediationPlan { - param($R, [bool]$ForceMode) - $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 } } - '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 } - } - '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 - # 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.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.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.Caution='Pozastaveno firmwarem; -Force se pokusí i tak.' } - } - 'NOT_SUPPORTED' { - $plan.Reason='ConfidenceLevel: Not Supported — zařízení nepodporuje automatickou cestu.' - $plan.NextSteps=@('Ověřte firmware update u OEM.','Pokud není, dokumentujte jako trvalou výjimku.') - } - 'SETUP_MODE' { - $plan.Reason='Secure Boot je v Setup Mode (chybí enrolled Platform Key) — aktualizaci nelze dokončit.' - $plan.NextSteps=@('V UEFI/BIOS obnovte výchozí Secure Boot klíče (enroll PK) / přepněte do User Mode.','Poté spusťte kontrolu znovu.') - } - 'BUILD_OUTDATED' { - $plan.Reason='OS má 2023 certifikáty v DB/KEK, ale chybí servicing pro aktivaci 2023 Boot Manageru (WindowsUEFICA2023Capable i UEFICA2023Status jsou prázdné). Boot Manager je stále podepsaný 2011 a tento build neumí nasadit nový — restarty ani remediace nepomohou.' - $plan.NextSteps=@('Nainstalujte nejnovější kumulativní update Windows (Windows Update) a restartujte.','Poté spusťte kontrolu znovu — pak by se Boot Manager měl dát aktualizovat.','Pozn.: velmi staré buildy (např. Server 2016 RTM) neobsahují 2023 boot-manager servicing.') - } - 'TASK_MISSING' { - $plan.Reason='Servicing task neexistuje — typicky chybí build z 10/2025 (KB5066835).' - $plan.NextSteps=@('Nainstalujte nejnovější kumulativní update Windows.','Poté spusťte kontrolu znovu.') - } - 'SECUREBOOT_DISABLED' { $plan.Reason='Secure Boot je vypnutý — zapnutí je rozhodnutí mimo rozsah skriptu.'; $plan.NextSteps=@('Zvažte zapnutí Secure Boot (pozor na BitLocker PCR7).') } - 'NO_SECUREBOOT*' { $plan.Reason='Secure Boot není podporováno (Legacy BIOS / VM bez vTPM).'; $plan.NextSteps=@('Dokumentujte jako výjimku.') } - default { $plan.Reason='Stav nebylo možné jednoznačně vyhodnotit.' } - } - return $plan -} +#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 ($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)$') } + $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 { - # Nabídne uživateli restart. Skript nikdy nerestartuje automaticky — vždy interaktivní dotaz. + 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 @@ -805,7 +883,7 @@ function Invoke-RestartOffer { 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) { + 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 @@ -820,34 +898,71 @@ function Invoke-RestartOffer { 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)' + Add-LogLine 'Restart: uživatel odložil restart (fallback)' } } } #endregion -#region ── Stav napříč restarty + RunOnce ───────────────────────────────────── +#region ── Předpoklady ──────────────────────────────────────────────────────── -function Get-ScriptPath { if ($PSCommandPath) { return $PSCommandPath }; if ($MyInvocation.MyCommand.Path) { return $MyInvocation.MyCommand.Path }; return $null } -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 } } -function Register-Resume { - $sp=Get-ScriptPath; if (-not $sp) { Write-Line ' (Nelze zjistit cestu skriptu — auto-recheck nenastaven.)' DarkGray; return } - $cmd='powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{0}" -CheckOnly' -f $sp - try { - if (-not (Test-Path $REG_RUNONCE)) { New-Item -Path $REG_RUNONCE -Force | Out-Null } - New-ItemProperty -Path $REG_RUNONCE -Name 'SecureBootCA2023Recheck' -Value $cmd -PropertyType String -Force | Out-Null - Write-Line ' Auto-recheck po příštím restartu nastaven (RunOnce, jen kontrola — žádný restart).' Green - } catch { Write-Line (" (Auto-recheck se nepodařilo nastavit: {0})" -f $_.Exception.Message) DarkGray } +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 @@ -855,181 +970,128 @@ function Register-Resume { #region ── Remediace ────────────────────────────────────────────────────────── function Invoke-Remediation { - param([bool]$SkipBootMgrFile) - $out=[ordered]@{ Status='Unknown'; Message=''; After=$null } - Write-Head 'REMEDIACE'; Add-LogLine 'REMEDIACE START' + $out = [ordered]@{ Status='Unknown'; Message=''; After=$null } + Write-Head 'REMEDIACE' + Add-LogLine 'REMEDIACE START' - # 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 + # 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) - $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 + $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 } + 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 - $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) } + $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) + } } - $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 } - Write-Host 'hotovo' -ForegroundColor Green - $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 } - else { - Write-Host ' [2/3] Spouštím servicing task a čekám na změnu stavu ' -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 ji zpracuje při servisním běhu.' DarkGray } - else { - $init=[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 - $el=0; $state='Running'; $changed=$false - do { - Start-Sleep -Seconds 3; $el+=3; Write-Host '.' -NoNewline -ForegroundColor DarkGray - $state=(Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue).State - if ([int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) -ne $init) { $changed=$true; break } - } while ($state -eq 'Running' -and $el -lt $TASK_TIMEOUT_SEC) - $now=[int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) - if ($changed) { Write-Host (" hotovo ({0}s, AvailableUpdates -> 0x{1:X})" -f $el,$now) -ForegroundColor Green } - elseif ($el -ge $TASK_TIMEOUT_SEC) { Write-Host (" timeout {0}s (stav {1})" -f $el,$state) -ForegroundColor Yellow } - else { Write-Host (" hotovo ({0}s, stav {1})" -f $el,$state) -ForegroundColor Green } - Add-LogLine ("Krok 2: stav=$state, elapsed=${el}s, AvailableUpdates=0x{0:X}" -f $now) - } catch { Write-Host '— nepodařilo se spustit' -ForegroundColor Yellow; Write-Line (" {0}" -f $_.Exception.Message) DarkGray } + $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 } } - Write-Host ' [3/3] Ověřuji nový stav (vč. Boot Manageru) ... ' -NoNewline + # Krok 3/3: Ověřit nový stav + Write-Host ' [3/3] Ověřuji nový stav ... ' -NoNewline Start-Sleep -Seconds 2 - $after=Invoke-Detection -SkipBootMgrFile:$SkipBootMgrFile - Write-Host 'hotovo' -ForegroundColor Green; Write-Host '' - Write-Line (' AvailableUpdates: {0}' -f (Get-AvailableUpdatesText $after.Registry.AvailableUpdates)) Cyan - 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.' + $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 ── Předpoklady + progress ──────────────────────────────────────────── +#region ── Stav napříč restarty ─────────────────────────────────────────────── -function Get-StageInfo { - param($v) - if ($null -eq $v) { return @{ Step=0; Total=6; Label='nezahájeno' } } - switch ([int]$v) { - 0x5944 { return @{ Step=1; Total=6; Label='naplánováno (start)' } } - 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 — 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) } } - } +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 Show-Progress { - param($R, $Prev) - if ($R.Category -notlike 'UPDATE*' -and $R.Category -notlike 'OK*') { return } - Write-Head 'PRŮBĚH (krok a změna od minulého běhu)' - $st = Get-StageInfo $R.Registry.AvailableUpdates - $bar = ('#' * $st.Step) + ('.' * ($st.Total - $st.Step)) - Write-Host (" [{0}] krok {1}/{2} — {3}" -f $bar, $st.Step, $st.Total, $st.Label) -ForegroundColor Cyan - if ($Prev) { - $pv = [string]$Prev.AvailableUpdates - $cv = '0x{0:X}' -f [int]$R.Registry.AvailableUpdates - if ($pv -eq $cv) { - Write-Host (" Beze změny od minulého běhu (#{0}, {1})." -f $Prev.Cycle, $Prev.Timestamp) -ForegroundColor Yellow - if ($R.Category -in @('UPDATE_PENDING','UPDATE_PARTIAL')) { - Write-Host ' -> Proces čeká na RESTART. Pokud jste restartoval, spusťte skript znovu; jinak server restartujte.' -ForegroundColor Yellow - } - } else { - Write-Host (" Od minulého běhu: AvailableUpdates {0} -> {1} (postup)" -f $pv, $cv) -ForegroundColor Green - } - } +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 Get-Prerequisites { - param($R, [bool]$IsAdmin) - $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"'} } - - $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"} } - - $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 -} - -function Show-Prerequisites { - param($Prereqs) - Write-Head 'PŘEDPOKLADY (lze proces dokončit?)' - 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 $p.Label -Note $p.Note -NoteColor $nc - } - if (@($Prereqs | Where-Object { $_.Hard -and -not $_.Ok }).Count) { - Write-Host ' ! Než spustíte aktualizaci, vyřešte výše označené řádky — jinak proces nelze dokončit.' -ForegroundColor Red - } else { - Write-Host ' Předpoklady splněny — jakmile spustíte, proces lze dokončit.' -ForegroundColor Green - } +function Clear-ResumeState { + if (Test-Path $STATE_FILE) { Remove-Item -LiteralPath $STATE_FILE -Force -ErrorAction SilentlyContinue } } #endregion @@ -1038,96 +1100,214 @@ function Show-Prerequisites { $isWhatIf = [bool]$WhatIfPreference +# Hlavička Write-Host '' Write-Rule 'Cyan' Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor White -Write-Host (" {0} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray +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} =====' -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $isAdmin, $CheckOnly.IsPresent, $AssumeYes.IsPresent, $Force.IsPresent) -if (-not $isAdmin) { Write-Host ''; Write-Host ' ! Neběží jako Administrator — část kontrol a remediace nebude dostupná.' -ForegroundColor Yellow } +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 } +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 +} -Write-Host ''; Write-Host ' Zjišťuji stav (vč. ověření Boot Manageru)…' -ForegroundColor DarkGray -$result = Invoke-Detection -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent +# Detekce stavu +Write-Host '' +Write-Host ' Zjišťuji stav serveru...' -ForegroundColor DarkGray +$result = Invoke-Detection $prereqs = Get-Prerequisites -R $result -IsAdmin $isAdmin -# Pořadí: SERVER -> PŘEDPOKLADY -> POSTUP AKTUALIZACE (checklist) -> PRŮBĚH -> registry -> VÝSLEDEK -Show-DetectionSummary -R $result -Prereqs $prereqs -Prev $prev -if ($Detailed) { Show-DetailedDetection -R $result } +# 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 - $code = switch -Wildcard ($result.Category) { - 'OK*' { 0 } 'UPDATE_NEEDED' { 1 } 'UPDATE_PARTIAL' { 1 } 'UPDATE_PENDING' { 1 } default { 2 } } - Write-Host (" CHECK: {0} (exit {1})" -f $result.CategoryLabel, $code) -ForegroundColor $result.CategoryColor + $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 - if ($PassThru) { $result } - exit $code + exit $exitCode } -$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent -Write-Head 'VYHODNOCENÍ' -if ($plan.Reason) { Write-Host (" {0}" -f $plan.Reason) -ForegroundColor Gray } +# ── 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 } -$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 } + +# ── 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 { - 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 } - elseif ($isWhatIf) { - Write-Host ''; Write-Host ' -WhatIf — co by remediace udělala (bez změn):' -ForegroundColor Cyan - Write-Host (" - MicrosoftUpdateManagedOptIn=1, AvailableUpdates=0x{0:X4}, HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE) -ForegroundColor Gray - if (-not $SkipScheduledTask) { Write-Host ' - spustila by servicing task a počkala na změnu stavu' -ForegroundColor Gray } - Write-Host ' - server by NErestartovala' -ForegroundColor Gray + # ── 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 { - $q='Chcete nyní zahájit / pokračovat v aktualizaci Secure Boot certifikátů?' - $d='Nastaví registry (KB5068202) a spustí servicing task. Server NEBUDE restartován.' - if (Get-UserConsent -Question $q -Detail $d) { - $cycle = $cycle + 1 - Add-LogLine ("Cyklus #{0} | {1} | kategorie={2}" -f $cycle, $result.Hostname, $result.Category) - $remediation = Invoke-Remediation -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent - Flush-Log - } else { Write-Host ''; Write-Host ' Remediace neprovedena (volba uživatele).' -ForegroundColor Yellow } + 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 - if ($after.Category -like 'OK*') { Write-Host (" {0} HOTOVO — server je kompletní." -f $SYM_DONE) -ForegroundColor Green } - else { - Write-Host ' ČÁSTEČNĚ HOTOVO — proces pokračuje po restartu.' -ForegroundColor Yellow - Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White - 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 } - Write-Host ' 3. Po restartu spusťte kontrolu znovu — dokud nebude zelené HOTOVO.' -ForegroundColor Gray - Write-Host ' Cíl: AvailableUpdates=0x0 · UEFICA2023Status=Updated · WinUEFICA2023Capable=2.' -ForegroundColor DarkGray - if ($RegisterResume) { Register-Resume } else { Write-Host ' (Tip: -RegisterResume spustí kontrolu po příštím restartu automaticky.)' -ForegroundColor DarkGray } + $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 } -else { Write-Host ' KONEC — bez změn na serveru.' -ForegroundColor Cyan } +} 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 '' - -# Stav pro příští běh (delta) + jednorázový zápis logu -$finalR = if ($remediation -and $remediation.After) { $remediation.After } else { $result } -if ($finalR.Category -like 'OK*') { Clear-ResumeState } else { Save-ResumeState -R $finalR -Cycle $cycle } Flush-Log -if ($PassThru) { return [pscustomobject]@{ Detection=$result; Plan=$plan; Remediation=$remediation } } - #endregion