From 50d3df5ef14d1719182a38788d803f6dfff9cdf4 Mon Sep 17 00:00:00 2001 From: Petr Stepan Date: Fri, 5 Jun 2026 17:15:59 +0200 Subject: [PATCH] Add Invoke-SecureBootRemediation.ps1 --- .DS_Store | Bin 8196 -> 8196 bytes Invoke-SecureBootRemediation.ps1 | 698 +++++++++++++++++++++++++++++++ 2 files changed, 698 insertions(+) create mode 100644 Invoke-SecureBootRemediation.ps1 diff --git a/.DS_Store b/.DS_Store index c24ff2b94e3be14a453b7a65032c81dd1135b626..7f9348d90648d5cc0dd9c9f3428dde44ed0c16a4 100644 GIT binary patch delta 47 zcmZp1XmOa}&&awlU^hP_>tr5*dz>l7$vH{+`8kZ6?+J*oZ)TVH#xi-fpyFmu(PvBm DdQ%U{ delta 39 vcmZp1XmOa}&&aYdU^hP_%VZvbdz=3YaIjBo5Z%l!@r`A(kmy&Yi48&k3%(73 diff --git a/Invoke-SecureBootRemediation.ps1 b/Invoke-SecureBootRemediation.ps1 new file mode 100644 index 0000000..854d5a7 --- /dev/null +++ b/Invoke-SecureBootRemediation.ps1 @@ -0,0 +1,698 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Detekce + interaktivní remediace Secure Boot certifikátů na jednom serveru (KB5062710 / KB5068202). + +.DESCRIPTION + Sloučení detekčního (Invoke-SecureBootAudit.ps1) a remediačního + (Set-SecureBootCertificateUpdate.ps1) skriptu do jednoho lokálního průvodce: + + 1. DETEKCE — zjistí prostředí, stav Secure Boot, přítomnost expirujících 2011 + a nových 2023 certifikátů, stav v registrech a Event Logu. + 2. ROZHODNUTÍ — vyhodnotí, zda lze remediaci aplikovat. Pokud ano, ZEPTÁ SE uživatele. + 3. REMEDIACE — pouze po souhlasu. Kroky probíhají SEKVENČNĚ; každý čeká na dokončení + předchozího (zápis registry se ověří read-backem, na servicing task se ČEKÁ). + 4. DALŠÍ KROKY — skript vypíše, co je třeba udělat dál. + + Skript ZÁMĚRNĚ NERESTARTUJE server. Restart je nutný pro dokončení aktualizace, + ale necháváme ho na plánovaném okně správce. + + Skript je určen k LOKÁLNÍMU spuštění na serveru (jako Administrator) nebo uvnitř + interaktivního Enter-PSSession. Pro hromadný/neinteraktivní audit flotily použijte + původní Start-SecureBootFleetAudit.ps1 / Set-SecureBootCertificateUpdate.ps1. + +.PARAMETER AssumeYes + Přeskočí interaktivní dotaz a remediaci rovnou aplikuje (pokud je smysluplná). + Vhodné pro neinteraktivní běh. Restart se stále NEPROVÁDÍ. + +.PARAMETER Force + Aplikuje remediaci i ve stavech, kdy ji skript jinak nedoporučuje + (firmware nezpůsobilý, již úspěšně dokončeno, Hyper-V Event 1795). + +.PARAMETER SkipScheduledTask + Nastaví registry, ale nespustí servicing task. Task se spustí sám + při příštím plánovaném běhu (cca každých 12 h). + +.PARAMETER Detailed + Vypíše rozšířený detekční rozpis (jednotlivé certifikáty, posledních N událostí). + +.PARAMETER PassThru + Vrátí výsledný objekt (detekce + případná remediace) do pipeline. + +.PARAMETER LogPath + Cesta k logu remediace. Default: log se vytvoří vedle skriptu pouze pokud se remediace + skutečně provede. Zadáním cesty vynutíte logování od začátku. + +.EXAMPLE + .\Invoke-SecureBootRemediation.ps1 + Interaktivní průběh — detekce, dotaz, případná remediace. + +.EXAMPLE + .\Invoke-SecureBootRemediation.ps1 -WhatIf + Pouze detekce + ukázka, co by remediace udělala. Žádné změny. + +.EXAMPLE + .\Invoke-SecureBootRemediation.ps1 -AssumeYes + Neinteraktivně aplikuje remediaci, pokud je smysluplná (bez restartu). + +.NOTES + Reference: KB5062710, KB5068202, KB5085046, KB5085790. + AvailableUpdates = 0x5944 (KEK + UEFI CA + Windows UEFI CA + Boot Manager) dle KB5068202. + Vyžaduje Administrator pro čtení UEFI databází a zápis do registry. +#> + +[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] +param( + [switch]$AssumeYes, + [switch]$Force, + [switch]$SkipScheduledTask, + [switch]$Detailed, + [switch]$PassThru, + [string]$LogPath +) + +$ErrorActionPreference = 'SilentlyContinue' +Set-StrictMode -Off + +# ── Konzole: vynutit UTF-8 výstup, aby diakritika fungovala i ve Windows PowerShell 5.1 ── +# Zdroják je uložen jako UTF-8 s BOM (PS 5.1 tak čte řetězce správně); tady sjednotíme i výstup. +# Při přesměrovaném výstupu (Invoke-Command, pipe do souboru) může vyhodit chybu → try/catch. +try { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [Console]::OutputEncoding = $utf8NoBom + $OutputEncoding = $utf8NoBom +} catch { } + +#region ── Konstanty ────────────────────────────────────────────────────────── + +$REG_SECUREBOOT = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot' +$REG_SERVICING = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' +$AVAILABLE_UPDATES_VALUE = 0x5944 # KB5068202: KEK + UEFI CA + Windows UEFI CA + Boot Manager +$TASK_PATH = '\Microsoft\Windows\PI\' +$TASK_NAME = 'Secure-Boot-Update' +$TASK_TIMEOUT_SEC = 180 + +#endregion + +#region ── Log / výpis ──────────────────────────────────────────────────────── + +$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } +$script:LogFile = if ($LogPath) { $LogPath } else { + Join-Path $scriptDir ("SecureBootRemediation-{0}-{1}.log" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyyMMdd-HHmmss')) +} +$script:LogActive = [bool]$LogPath # při explicitním -LogPath logujeme od začátku + +function Add-LogLine { + param([string]$Text) + if (-not $script:LogActive) { return } + try { ('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Text) | + Out-File -FilePath $script:LogFile -Append -Encoding UTF8 -Force } catch { } +} + +function Write-Line { + # Stručný výpis na konzoli + (volitelně) do logu. + param([string]$Text = '', [string]$Color = 'Gray', [switch]$NoLog) + if ($Color) { Write-Host $Text -ForegroundColor $Color } else { Write-Host $Text } + if (-not $NoLog) { Add-LogLine $Text } +} + +function Write-Rule { Write-Host ('=' * 62) -ForegroundColor DarkCyan } + +#endregion + +#region ── Detekce (z Invoke-SecureBootAudit.ps1) ───────────────────────────── + +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' } + return 'Physical' + } catch { return 'Unknown' } +} + +function Get-HardwareInfo { + $hw = [ordered]@{ Manufacturer='Unknown'; Model='Unknown'; SerialNumber='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 + $hw.SerialNumber = [string]$bios.SerialNumber + if ($bios.ReleaseDate) { $hw.BiosReleaseDate = ([datetime]$bios.ReleaseDate).ToString('yyyy-MM-dd') } + } catch { } + $fwTypeKey = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue + if ($fwTypeKey) { + $hw.FirmwareType = if ($fwTypeKey.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 { + $state = [ordered]@{ IsUEFI=$false; IsSupported=$false; IsEnabled=$false; ConfirmResult='Unknown'; Error=$null } + $fwTypeKey = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue + $state.IsUEFI = ($fwTypeKey -and $fwTypeKey.PEFirmwareType -eq 2) -or (Test-Path $REG_SECUREBOOT) + if (-not $state.IsUEFI) { return $state } + try { + $result = Confirm-SecureBootUEFI -ErrorAction Stop + $state.IsSupported = $true; $state.IsEnabled = [bool]$result; $state.ConfirmResult = $result.ToString() + } catch { + $msg = $_.Exception.Message + if ($msg -like '*not supported*' -or $msg -like '*Cmdlet not supported*') { + $state.IsSupported = $false; $state.ConfirmResult = 'NotSupported' + } elseif ($msg -like '*disabled*') { + $state.IsSupported = $true; $state.IsEnabled = $false; $state.ConfirmResult = 'Disabled' + } else { + $state.IsSupported = $true; $state.IsEnabled = $false; $state.ConfirmResult = 'Error'; $state.Error = $msg + } + } + return $state +} + +function Parse-EFISignatureList { + param([byte[]]$Bytes) + $certs = @() + if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs } + $X509_GUID = [byte[]](0xa1,0x59,0xc0,0xa5, 0xe4,0x94, 0xa7,0x4a, 0x87,0xb5,0xab,0x15,0x5c,0x2b,0xf0,0x72) + $offset = 0 + while ($offset + 28 -le $Bytes.Length) { + $sigTypeGUID = $Bytes[$offset..($offset + 15)] + $sigListSize = [BitConverter]::ToUInt32($Bytes, $offset + 16) + $sigHeaderSize = [BitConverter]::ToUInt32($Bytes, $offset + 20) + $sigSize = [BitConverter]::ToUInt32($Bytes, $offset + 24) + if ($sigListSize -lt 28 -or $sigListSize -gt ($Bytes.Length - $offset)) { break } + $isX509 = $true + for ($i = 0; $i -lt 16; $i++) { if ($sigTypeGUID[$i] -ne $X509_GUID[$i]) { $isX509 = $false; break } } + if ($isX509 -and $sigSize -gt 16) { + $sigOffset = $offset + 28 + $sigHeaderSize + $listEnd = $offset + $sigListSize + while ($sigOffset + $sigSize -le $listEnd) { + $certOffset = $sigOffset + 16 + $certSize = [int]$sigSize - 16 + if ($certOffset + $certSize -le $Bytes.Length -and $certSize -gt 0) { + $certBytes = $Bytes[$certOffset..($certOffset + $certSize - 1)] + try { $certs += New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(, [byte[]]$certBytes) } catch { } + } + $sigOffset += $sigSize + } + } + $offset += $sigListSize + } + return $certs +} + +function Convert-CertToInfo { + param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert) + return [ordered]@{ Subject=$Cert.Subject; Thumbprint=$Cert.Thumbprint + NotBefore=$Cert.NotBefore.ToString('yyyy-MM-dd'); NotAfter=$Cert.NotAfter.ToString('yyyy-MM-dd') } +} + +function Get-CertificateStatus { + $status = [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 } + AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false + } + try { + $kekObj = Get-SecureBootUEFI -Name KEK -ErrorAction Stop + $kekCerts = Parse-EFISignatureList -Bytes $kekObj.Bytes + foreach ($cert in $kekCerts) { + $subj = $cert.Subject; $info = Convert-CertToInfo $cert + if ($subj -like '*KEK CA 2011*') { $status.KEK.Has2011 = $true; $status.KEK.Certs2011 += $info } + if ($subj -like '*KEK 2K CA 2023*' -or ($subj -like '*KEK*CA 2023*')) { $status.KEK.Has2023 = $true; $status.KEK.Certs2023 += $info } + } + } catch { $status.KEK.Error = $_.Exception.Message } + try { + $dbObj = Get-SecureBootUEFI -Name db -ErrorAction Stop + $dbCerts = Parse-EFISignatureList -Bytes $dbObj.Bytes + foreach ($cert in $dbCerts) { + $subj = $cert.Subject; $info = Convert-CertToInfo $cert + if ($subj -like '*UEFI CA 2011*') { $status.DB.Has2011UEFI = $true; $status.DB.Certs2011 += $info } + if ($subj -like '*Windows Production PCA 2011*' -or $subj -like '*Windows PCA 2011*') { $status.DB.Has2011WindowsPCA = $true; $status.DB.Certs2011 += $info } + if ($subj -like '*UEFI CA 2023*' -and $subj -notlike '*Option ROM*' -and $subj -notlike '*Windows UEFI*') { $status.DB.Has2023UEFI = $true; $status.DB.Certs2023 += $info } + if ($subj -like '*Option ROM UEFI CA 2023*') { $status.DB.Has2023OptionROM = $true; $status.DB.Certs2023 += $info } + if ($subj -like '*Windows UEFI CA 2023*') { $status.DB.Has2023WindowsUEFI = $true; $status.DB.Certs2023 += $info } + } + } catch { $status.DB.Error = $_.Exception.Message } + $status.AnyExpiring2011 = $status.KEK.Has2011 -or $status.DB.Has2011UEFI -or $status.DB.Has2011WindowsPCA + $status.AllReplacements2023 = $status.KEK.Has2023 -and $status.DB.Has2023UEFI -and $status.DB.Has2023OptionROM -and $status.DB.Has2023WindowsUEFI + $status.AllReplacements2023_VM = $status.KEK.Has2023 -and $status.DB.Has2023WindowsUEFI + return $status +} + +function Get-RegistryStatus { + $reg = [ordered]@{ + SecureBootEnabled=$null; AvailableUpdates=$null; HighConfidenceOptOut=$null + ServicingKeyExists=$false; UEFICA2023Status=$null; UEFICA2023StatusText='KeyNotPresent' + UEFICA2023Error=$null; WindowsUEFICA2023Capable=$null + } + $mainProps = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue + if ($mainProps) { + $reg.SecureBootEnabled = $mainProps.SecureBootEnabled + $reg.AvailableUpdates = $mainProps.AvailableUpdates + $reg.HighConfidenceOptOut = $mainProps.HighConfidenceOptOut + } + $svcProps = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue + if ($svcProps) { + $reg.ServicingKeyExists = $true + $reg.UEFICA2023Status = $svcProps.UEFICA2023Status + $reg.UEFICA2023Error = $svcProps.UEFICA2023Error + $reg.WindowsUEFICA2023Capable = $svcProps.WindowsUEFICA2023Capable + $reg.UEFICA2023StatusText = switch ($reg.UEFICA2023Status) { + 0 { 'NotStarted' } 1 { 'InProgress' } 2 { 'Success' } 3 { 'Failed' } + $null { 'KeyNotPresent' } default { "Unknown ($($reg.UEFICA2023Status))" } + } + } + return $reg +} + +function Get-EventLogStatus { + $evtStatus = [ordered]@{ LastEventId=$null; LastEventTime=$null; ConfidenceLevel='NoRelevantEvents'; RelevantEvents=@(); Error=$null } + $relevantIds = @(1795,1796,1800,1801,1802,1803,1808) + try { + $events = Get-WinEvent -FilterHashtable @{ LogName='System'; Id=$relevantIds } -MaxEvents 20 -ErrorAction Stop + if ($events) { + $sorted = $events | Sort-Object TimeCreated -Descending + foreach ($evt in ($sorted | Select-Object -First 8)) { + $evtStatus.RelevantEvents += [ordered]@{ + EventId=$evt.Id; TimeCreated=$evt.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); Level=$evt.LevelDisplayName } + } + $last = $sorted | Select-Object -First 1 + $evtStatus.LastEventId = $last.Id + $evtStatus.LastEventTime = $last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') + $evtStatus.ConfidenceLevel = switch ($last.Id) { + 1808 { 'HighConfidence-Success' } 1801 { 'HighConfidence-Failed' } + 1795 { 'Failed-HyperVKnownIssue' } 1802 { 'Pending' } 1803 { 'InProgress' } default { 'Informational' } } + } + } catch { + if ($_.CategoryInfo.Reason -eq 'NoMatchingEventsException') { $evtStatus.ConfidenceLevel = 'NoRelevantEvents' } + else { $evtStatus.Error = $_.Exception.Message; $evtStatus.ConfidenceLevel = 'EventLogError' } + } + return $evtStatus +} + +function Get-RemediationCategory { + param($Result) + $sb = $Result.SecureBoot; $cert = $Result.Certificates; $reg = $Result.Registry; $env = $Result.EnvironmentType + if (-not $sb.IsUEFI -or -not $sb.IsSupported) { + if ($env -like '*VM*') { return @{ Code='NO_SECUREBOOT_VM'; Emoji='[X]'; Label='Secure Boot nepodporováno (VM bez vTPM/UEFI)' } } + return @{ Code='NO_SECUREBOOT'; Emoji='[X]'; Label='Secure Boot nepodporováno (Legacy BIOS)' } + } + if (-not $sb.IsEnabled) { return @{ Code='SECUREBOOT_DISABLED'; Emoji='[OFF]'; Label='Secure Boot vypnuto' } } + $isVM = $env -like '*VM*' + $certOK = if ($isVM) { $cert.AllReplacements2023_VM } else { $cert.AllReplacements2023 } + $certOKFull = $cert.AllReplacements2023 + if ($certOK -and -not $cert.AnyExpiring2011) { + $lbl = if ($isVM -and -not $certOKFull) { 'OK — má povinné 2023 certifikáty pro VM' } else { 'OK — má nové 2023 certifikáty' } + return @{ Code='OK'; Emoji='[OK]'; Label=$lbl } + } + if ($certOK -and $cert.AnyExpiring2011) { + $lbl = if ($isVM -and -not $certOKFull) { 'OK — přechodný stav, VM má povinné 2023 certifikáty' } else { 'OK — přechodný stav (2023 i 2011 certifikáty)' } + return @{ Code='OK_TRANSITION'; Emoji='[OK]'; Label=$lbl } + } + if ($reg.UEFICA2023Status -eq 3 -or $Result.EventLog.LastEventId -eq 1795 -or $Result.EventLog.LastEventId -eq 1801) { + return @{ Code='UPDATE_FAILED'; Emoji='[FAIL]'; Label='Selhání aktualizace certifikátů' } + } + if ($reg.UEFICA2023Status -eq 1 -or $reg.UEFICA2023Status -eq 2) { + return @{ Code='UPDATE_PENDING'; Emoji='[WAIT]'; Label='Aktualizace připravena, čeká na restart' } + } + if ($null -ne $reg.WindowsUEFICA2023Capable -and $reg.WindowsUEFICA2023Capable -eq 0) { + return @{ Code='FIRMWARE_UPDATE_NEEDED'; Emoji='[FW]'; Label='Čeká na firmware update (OEM)' } + } + return @{ Code='UPDATE_NEEDED'; Emoji='[!]'; Label='Nutná aktualizace certifikátů' } +} + +function Invoke-Detection { + $auditStart = Get-Date + $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue + $result = [ordered]@{ + AuditTimestamp = $auditStart.ToString('yyyy-MM-dd HH:mm:ss') + Hostname = $env:COMPUTERNAME + OSCaption = if ($osInfo) { $osInfo.Caption } else { $null } + OSBuild = if ($osInfo) { $osInfo.BuildNumber } else { $null } + EnvironmentType = Get-EnvironmentType + Hardware = Get-HardwareInfo + SecureBoot = Get-SecureBootState + Certificates = $null + Registry = Get-RegistryStatus + EventLog = Get-EventLogStatus + Category = $null + CategoryLabel = $null + } + if ($result.SecureBoot.IsUEFI -and $result.SecureBoot.IsSupported) { + $result.Certificates = Get-CertificateStatus + } else { + $result.Certificates = [ordered]@{ + KEK = [ordered]@{ Has2011=$false; Has2023=$false; Certs2011=@(); Certs2023=@(); Error='Secure Boot nedostupný' } + DB = [ordered]@{ Has2011UEFI=$false; Has2011WindowsPCA=$false; Has2023UEFI=$false + Has2023OptionROM=$false; Has2023WindowsUEFI=$false; Certs2011=@(); Certs2023=@(); Error='Secure Boot nedostupný' } + AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false } + } + $cat = Get-RemediationCategory -Result $result + $result.Category = $cat.Code + $result.CategoryLabel = "$($cat.Emoji) $($cat.Label)" + return $result +} + +#endregion + +#region ── Stručný výpis detekce ────────────────────────────────────────────── + +function Show-DetectionSummary { + param($R) + $sb = $R.SecureBoot; $c = $R.Certificates; $reg = $R.Registry; $evt = $R.EventLog; $hw = $R.Hardware + + Write-Line '' + Write-Line 'DETEKCE' Yellow + Write-Line (" Server : {0} ({1}, build {2})" -f $R.Hostname, $R.OSCaption, $R.OSBuild) + Write-Line (" Prostředí : {0} [{1} {2}]" -f $R.EnvironmentType, $hw.Manufacturer, $hw.Model) + + $sbText = if (-not $sb.IsUEFI) { 'Legacy BIOS — nepodporováno' } + elseif (-not $sb.IsSupported) { 'UEFI, ale Secure Boot nepodporováno' } + elseif ($sb.IsEnabled) { 'zapnuto (UEFI)' } else { 'vypnuto (UEFI)' } + Write-Line (" Secure Boot : {0}" -f $sbText) + + if ($sb.IsUEFI -and $sb.IsSupported) { + $l2011 = @() + if ($c.KEK.Has2011) { $l2011 += 'KEK CA 2011' } + if ($c.DB.Has2011UEFI) { $l2011 += 'UEFI CA 2011' } + if ($c.DB.Has2011WindowsPCA) { $l2011 += 'Windows PCA 2011' } + $l2023 = @() + if ($c.KEK.Has2023) { $l2023 += 'KEK 2K CA 2023' } + if ($c.DB.Has2023UEFI) { $l2023 += 'UEFI CA 2023' } + if ($c.DB.Has2023OptionROM) { $l2023 += 'Option ROM 2023' } + if ($c.DB.Has2023WindowsUEFI) { $l2023 += 'Windows UEFI CA 2023' } + + $t2011 = if ($l2011.Count) { ('přítomny ({0}) — expirují 6–10/2026' -f ($l2011 -join ', ')) } else { 'nepřítomny' } + $t2023 = if ($l2023.Count) { ('přítomny ({0})' -f ($l2023 -join ', ')) } else { 'CHYBÍ' } + Write-Line (" Certifikáty 11 : {0}" -f $t2011) + Write-Line (" Certifikáty 23 : {0}" -f $t2023) + Write-Line (" Registry stav : UEFICA2023Status = {0} ({1})" -f $reg.UEFICA2023Status, $reg.UEFICA2023StatusText) + if ($null -ne $reg.UEFICA2023Error) { Write-Line (" UEFICA2023Error = {0}" -f $reg.UEFICA2023Error) Red } + if ($evt.LastEventId) { Write-Line (" Poslední event : EventID {0} ({1})" -f $evt.LastEventId, $evt.LastEventTime) } + } + + $catColor = switch -Wildcard ($R.Category) { + 'OK*' { 'Green' } 'UPDATE_NEEDED' { 'Yellow' } 'UPDATE_PENDING' { 'Cyan' } + 'UPDATE_FAILED' { 'Red' } 'FIRMWARE*' { 'Magenta' } default { 'Gray' } } + Write-Line '' + Write-Line (" VÝSLEDEK: {0}" -f $R.CategoryLabel) $catColor +} + +function Show-DetailedDetection { + param($R) + $c = $R.Certificates; $evt = $R.EventLog + Write-Line '' + Write-Line 'PODROBNOSTI' DarkYellow + foreach ($grp in @(@{N='KEK';O=$c.KEK}, @{N='DB';O=$c.DB})) { + $all = @($grp.O.Certs2011) + @($grp.O.Certs2023) + foreach ($ci in $all) { + Write-Line (" [{0}] {1}" -f $grp.N, $ci.Subject) DarkGray + Write-Line (" platnost do {0} | thumbprint {1}" -f $ci.NotAfter, $ci.Thumbprint) DarkGray + } + } + if ($evt.RelevantEvents.Count) { + Write-Line ' Poslední relevantní události:' DarkGray + foreach ($e in ($evt.RelevantEvents | Select-Object -First 5)) { + $ec = if ($e.EventId -eq 1808) { 'Green' } elseif ($e.EventId -in @(1795,1801)) { 'Red' } else { 'DarkGray' } + Write-Line (" [{0}] EventID {1} {2}" -f $e.TimeCreated, $e.EventId, $e.Level) $ec + } + } +} + +#endregion + +#region ── Vyhodnocení remediace ────────────────────────────────────────────── + +function Resolve-RemediationPlan { + <# Vrátí plán: zda lze remediovat, zda se ptát, a jaké jsou další kroky. #> + param($R, [bool]$ForceMode) + $plan = [ordered]@{ Applicable=$false; AskUser=$false; Caution=$null; Reason=$null; NextSteps=@() } + $evtId = $R.EventLog.LastEventId + + switch -Wildcard ($R.Category) { + 'OK' { + $plan.Reason = 'Server má potřebné 2023 certifikáty — žádná akce není nutná.' + if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Server je již OK — -Force vynutí opětovné nastavení.' } + } + 'OK_TRANSITION' { + $plan.Reason = 'Server má nové 2023 certifikáty (a dosud i staré 2011) — žádná akce, jen monitorovat.' + if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Přechodný stav je v pořádku — -Force vynutí opětovné nastavení.' } + } + 'UPDATE_NEEDED' { + $plan.Applicable=$true; $plan.AskUser=$true + $plan.Reason='Lze aplikovat remediaci metodou registry (KB5068202).' + } + 'UPDATE_PENDING' { + $plan.Reason='Aktualizace je už připravená (UEFICA2023Status). Chybí pouze restart — což skript záměrně neprovádí.' + $plan.NextSteps=@('Naplánujte RESTART serveru.', 'Po restartu ověřte EventID 1808 v System logu.') + } + 'UPDATE_FAILED' { + if ($evtId -eq 1795) { + $plan.Reason='Selhání s EventID 1795 (známý problém Hyper-V). Příčina je na straně HOSTITELE, ne této VM.' + $plan.NextSteps=@('Aktualizujte Windows Server na Hyper-V hostiteli (KB5085790, fix od 3/2026).','Poté spusťte tuto kontrolu na VM znovu.') + if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='EventID 1795 = problém hostitele. -Force pouze zopakuje pokus na VM.' } + } else { + $plan.Applicable=$true; $plan.AskUser=$true + $plan.Reason='Předchozí pokus selhal (EventID 1801 / status Failed). Lze zopakovat metodou registry.' + $plan.Caution='Před opakováním zkontrolujte UEFICA2023Error (viz detekce výše).' + } + } + 'FIRMWARE_UPDATE_NEEDED' { + $plan.Reason='WindowsUEFICA2023Capable=0 — firmware nemusí nové certifikáty podporovat.' + $plan.NextSteps=@('Aktualizujte firmware serveru u výrobce (Dell/HP/Lenovo/Supermicro).','Pokud update není dostupný, dokumentujte jako trvalou výjimku.') + if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Firmware se jeví jako nezpůsobilý — -Force se pokusí i tak (může selhat).' } + } + 'SECUREBOOT_DISABLED' { + $plan.Reason='Secure Boot existuje, ale je vypnutý. Zapnutí je rozhodnutí mimo rozsah tohoto skriptu.' + $plan.NextSteps=@('Rozhodněte o zapnutí Secure Boot v UEFI/nastavení VM (pozor na BitLocker PCR7).') + } + 'NO_SECUREBOOT*' { + $plan.Reason='Secure Boot není podporováno (Legacy BIOS nebo VM bez vTPM/UEFI).' + $plan.NextSteps=@('Dokumentujte server jako výjimku — remediace certifikátů zde nedává smysl.') + } + default { $plan.Reason='Stav nebylo možné jednoznačně vyhodnotit.' } + } + return $plan +} + +function Get-UserConsent { + param([string]$Question, [string]$Detail) + if ($AssumeYes) { Write-Line (" {0} → automaticky ANO (-AssumeYes)" -f $Question) Cyan; return $true } + if (-not [Environment]::UserInteractive) { + Write-Line ' Neinteraktivní relace bez -AssumeYes — remediace se neprovede.' Yellow + return $false + } + Write-Host '' + Write-Host $Question -ForegroundColor White + if ($Detail) { Write-Host $Detail -ForegroundColor DarkGray } + try { + $yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Ano', 'Aplikovat remediaci (bez restartu)' + $no = New-Object System.Management.Automation.Host.ChoiceDescription '&Ne', 'Neprovádět žádné změny' + $choice = $Host.UI.PromptForChoice('', 'Vaše volba:', [System.Management.Automation.Host.ChoiceDescription[]]@($yes,$no), 1) + return ($choice -eq 0) + } catch { + $ans = Read-Host 'Aplikovat? [a/N]' + return ($ans -match '^(a|ano|y|yes)$') + } +} + +#endregion + +#region ── Remediace (sekvenčně, s čekáním; z Set-SecureBootCertificateUpdate.ps1) ── + +function Invoke-Remediation { + <# Sekvenční remediace. Každý krok čeká na dokončení předchozího. Bez restartu. #> + $outcome = [ordered]@{ Status='Unknown'; Message=''; FinalStatusText='Unknown'; LogFile=$script:LogFile } + + Write-Line '' + Write-Line 'REMEDIACE' Yellow + Add-LogLine 'REMEDIACE START' + + # ── Krok 1/3 — registry (a ověření zápisu read-backem) ── + Write-Host ' [1/3] Nastavuji registry klíče (AvailableUpdates = 0x5944) ... ' -NoNewline + try { + if (-not (Test-Path $REG_SECUREBOOT)) { New-Item -Path $REG_SECUREBOOT -Force -ErrorAction Stop | Out-Null } + Set-ItemProperty -Path $REG_SECUREBOOT -Name 'AvailableUpdates' -Value $AVAILABLE_UPDATES_VALUE -Type DWord -Force -ErrorAction Stop + + $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 } + + # Read-back: další krok pustíme jen když je hodnota skutečně zapsaná + $verify = (Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction Stop).AvailableUpdates + if ($verify -ne $AVAILABLE_UPDATES_VALUE) { throw "Ověření selhalo — AvailableUpdates = $verify (očekáváno $AVAILABLE_UPDATES_VALUE)" } + + Write-Host 'hotovo' -ForegroundColor Green + Add-LogLine ("Krok 1: AvailableUpdates=0x{0:X4} zapsáno a ověřeno; HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE) + } catch { + Write-Host 'CHYBA' -ForegroundColor Red + Write-Line (" {0}" -f $_.Exception.Message) Red + $outcome.Status='Error'; $outcome.Message="Zápis registry selhal: $($_.Exception.Message)" + Add-LogLine "Krok 1 CHYBA: $($_.Exception.Message)" + return $outcome + } + + # ── Krok 2/3 — servicing task a ČEKÁNÍ na dokončení ── + if ($SkipScheduledTask) { + Write-Line ' [2/3] Servicing task přeskočen (-SkipScheduledTask) — spustí se sám (cca á 12 h).' DarkGray + Add-LogLine 'Krok 2: task přeskočen (-SkipScheduledTask)' + } else { + Write-Host ' [2/3] Spouštím servicing task a čekám na dokončení ' -NoNewline + $task = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue + if (-not $task) { + Write-Host '— task nenalezen' -ForegroundColor Yellow + Write-Line ' Task neexistuje; registry je nastavena, Windows ji zpracuje při příštím servisním běhu.' DarkGray + Add-LogLine 'Krok 2: scheduled task nenalezen — registry zpracuje Windows samostatně' + } else { + try { + Start-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop + Start-Sleep -Seconds 2 # dát tasku čas přejít do stavu Running + $elapsed = 0; $state = 'Running' + do { + Start-Sleep -Seconds 3; $elapsed += 3 + Write-Host '.' -NoNewline -ForegroundColor DarkGray + $state = (Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue).State + } while ($state -eq 'Running' -and $elapsed -lt $TASK_TIMEOUT_SEC) + + $info = Get-ScheduledTaskInfo -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue + if ($state -eq 'Running' -and $elapsed -ge $TASK_TIMEOUT_SEC) { + Write-Host (' stále běží po {0}s' -f $elapsed) -ForegroundColor Yellow + Write-Line ' Task neskončil v limitu — pokračuji. Stav ověřte později.' Yellow + Add-LogLine "Krok 2: task po ${elapsed}s stále Running (timeout)" + } else { + $rc = if ($info) { '0x{0:X}' -f $info.LastTaskResult } else { 'n/a' } + Write-Host (' hotovo ({0}s, stav: {1}, výsledek: {2})' -f $elapsed, $state, $rc) -ForegroundColor Green + Add-LogLine "Krok 2: task dokončen za ${elapsed}s, stav=$state, LastTaskResult=$rc" + } + } catch { + Write-Host '— nepodařilo se spustit' -ForegroundColor Yellow + Write-Line (" {0}" -f $_.Exception.Message) DarkGray + Write-Line ' Registry je nastavena; task se spustí sám při příštím běhu (cca á 12 h).' DarkGray + Add-LogLine "Krok 2: spuštění tasku selhalo: $($_.Exception.Message)" + } + } + } + + # ── Krok 3/3 — ověření výsledného stavu (až po dokončení tasku) ── + Write-Host ' [3/3] Ověřuji výsledný stav ... ' -NoNewline + Start-Sleep -Seconds 2 + $post = Get-RegistryStatus + $outcome.FinalStatusText = $post.UEFICA2023StatusText + Write-Host ('UEFICA2023Status = {0} ({1})' -f $post.UEFICA2023Status, $post.UEFICA2023StatusText) -ForegroundColor Cyan + Add-LogLine ("Krok 3: UEFICA2023Status={0} ({1})" -f $post.UEFICA2023Status, $post.UEFICA2023StatusText) + if ($null -ne $post.UEFICA2023Error) { Write-Line (" UEFICA2023Error = {0}" -f $post.UEFICA2023Error) Red } + + $outcome.Status = 'Applied' + $outcome.Message = 'Registry nastavena a ověřena, servicing task zpracován. Certifikáty se aplikují až při restartu.' + Add-LogLine 'REMEDIACE KONEC: Applied' + return $outcome +} + +#endregion + +#region ── Main ─────────────────────────────────────────────────────────────── + +$isWhatIf = [bool]$WhatIfPreference + +Write-Host '' +Write-Rule +Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor Cyan +Write-Host (" {0} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray +Write-Rule + +# Admin check (čtení UEFI db a zápis registry vyžaduje elevaci) +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +if (-not $isAdmin) { + Write-Line '' + Write-Line ' ! Skript neběží jako Administrator — některé hodnoty nemusí být dostupné a remediaci nelze provést.' Yellow +} + +# ── 1) DETEKCE ── +$result = Invoke-Detection +Show-DetectionSummary -R $result +if ($Detailed) { Show-DetailedDetection -R $result } + +# ── 2) VYHODNOCENÍ ── +$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent + +Write-Line '' +Write-Line 'VYHODNOCENÍ' Yellow +if ($plan.Reason) { Write-Line (" {0}" -f $plan.Reason) } + +$remediation = $null + +if (-not $plan.Applicable) { + # Nelze / netřeba remediovat — vypsat jasné další kroky + if ($plan.NextSteps.Count) { + Write-Line '' + Write-Line ' Další kroky:' White + $i = 1; foreach ($s in $plan.NextSteps) { Write-Line (" {0}. {1}" -f $i, $s); $i++ } + } else { + Write-Line ' Není potřeba žádná akce.' Green + } +} else { + if ($plan.Caution) { Write-Line (" Pozor: {0}" -f $plan.Caution) Yellow } + + if (-not $isAdmin) { + Write-Line '' + Write-Line ' Remediaci nelze provést bez práv Administrator. Spusťte skript elevovaně.' Yellow + } elseif ($isWhatIf) { + Write-Line '' + Write-Line ' -WhatIf — co by remediace udělala (žádné změny se neprovádějí):' Cyan + Write-Line (" • nastavila by AvailableUpdates = 0x{0:X4} a HighConfidenceOptOut = 0" -f $AVAILABLE_UPDATES_VALUE) + if (-not $SkipScheduledTask) { Write-Line (" • spustila by servicing task '{0}{1}' a počkala na jeho dokončení" -f $TASK_PATH, $TASK_NAME) } + Write-Line ' • server by NErestartovala' + } else { + # ── 3) DOTAZ + REMEDIACE ── + $q = 'Chcete nyní zahájit aktualizaci Secure Boot certifikátů?' + $d = 'Nastaví se registry klíče (KB5068202) a spustí servicing task. Server NEBUDE restartován.' + if (Get-UserConsent -Question $q -Detail $d) { + $script:LogActive = $true # od teď logujeme do souboru + Add-LogLine ("Detekce: {0} | kategorie={1}" -f $result.Hostname, $result.Category) + $remediation = Invoke-Remediation + } else { + Write-Line '' + Write-Line ' Remediace neprovedena (volba uživatele). Žádné změny.' Yellow + } + } +} + +# ── 4) ZÁVĚR / DALŠÍ KROKY ── +Write-Line '' +Write-Rule +if ($remediation -and $remediation.Status -eq 'Applied') { + Write-Line 'HOTOVO — aktualizace je připravena.' Green + Write-Line '' + Write-Line 'Další kroky:' White + Write-Line ' 1. Naplánujte RESTART serveru — skript jej záměrně neprovedl.' + Write-Line ' 2. Pozor na BitLocker: je-li aktivní s PCR7, před restartem ověřte recovery key.' + Write-Line ' 3. Po restartu ověřte úspěch — EventID 1808 v System logu:' + Write-Line " Get-WinEvent -FilterHashtable @{LogName='System';Id=1808} -MaxEvents 3" DarkGray + Write-Line ' 4. Volitelně spusťte tuto kontrolu znovu — očekávaný výsledek: OK.' + Write-Line '' + Write-Line ("Log: {0}" -f $script:LogFile) DarkGray +} elseif ($remediation -and $remediation.Status -eq 'Error') { + Write-Line ('CHYBA REMEDIACE — {0}' -f $remediation.Message) Red + Write-Line ("Log: {0}" -f $script:LogFile) DarkGray +} else { + Write-Line 'KONEC — bez změn na serveru.' Cyan +} +Write-Rule +Write-Host '' + +if ($PassThru) { + return [pscustomobject]@{ Detection = $result; Plan = $plan; Remediation = $remediation } +} + +#endregion