Add Invoke-SecureBootRemediation.ps1

This commit is contained in:
Petr Stepan
2026-06-05 17:15:59 +02:00
parent d148ec05ae
commit 50d3df5ef1
2 changed files with 698 additions and 0 deletions
+698
View File
@@ -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í 610/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