Files
SecureBootCA2023/Invoke-SecureBootRemediation.ps1
T
Petr Stepan da2b734aa1 v2
2026-06-06 19:41:25 +02:00

1314 lines
58 KiB
PowerShell

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