Files
SecureBootCA2023/Invoke-SecureBootRemediation.ps1
T
2026-06-05 18:23:36 +02:00

967 lines
50 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Detekce + interaktivní remediace Secure Boot certifikátů na jednom serveru (KB5062710 / KB5068202).
.DESCRIPTION
Sloučení detekčního a remediačního skriptu do jednoho lokálního průvodce s fázovým
checklistem. Server prohlásí za HOTOVÝ teprve když jsou splněné VŠECHNY povinné fáze —
včetně ověření, že Boot Manager (bootmgfw.efi) je reálně podepsaný Windows UEFI CA 2023.
Pouhá přítomnost certifikátů v KEK/DB nestačí (to byl důvod předčasného „OK").
Fáze (checklist):
1. Secure Boot zapnutý
2. Servicing task k dispozici
3. KEK obsahuje Microsoft Corporation KEK 2K CA 2023
4. DB obsahuje Windows UEFI CA 2023
5. Boot Manager podepsaný Windows UEFI CA 2023 (mount ESP + certutil / Event 1799)
+ volitelné: 3rd-party UEFI CA 2023, Option ROM 2023, DBX revokace PCA 2011
Skript ZÁMĚRNĚ NERESTARTUJE server. Sleduje stav i napříč restarty (state.json) a umí
volitelně po dalším restartu spustit kontrolu automaticky (-RegisterResume) — stále BEZ
auto-restartu.
.PARAMETER CheckOnly
Jen detekce + checklist, žádné změny ani dotaz. Nastaví exit kód: 0=hotovo, 1=nutná akce,
2=blokováno (firmware/OEM/Event 1803/1795). Vhodné pro RMM/monitoring a pro auto-recheck.
.PARAMETER AssumeYes
Přeskočí interaktivní dotaz a remediaci rovnou aplikuje (pokud je smysluplná). Bez restartu.
.PARAMETER Force
Aplikuje remediaci i ve stavech, kdy ji skript jinak nedoporučuje (firmware nezpůsobilý,
už hotovo, Hyper-V Event 1795).
.PARAMETER SkipScheduledTask
Nastaví registry, ale nespustí servicing task (spustí se sám cca á 12 h).
.PARAMETER SkipBootManagerFileCheck
Neověřuje podpis bootmgfw.efi přes mount ESP + certutil. Použije jen Event 1799.
Rychlejší a méně invazivní; o něco méně spolehlivé.
.PARAMETER RegisterResume
Po remediaci, kde zbývá restart, zaregistruje RunOnce, který po PŘÍŠTÍM (ručním) restartu
automaticky spustí tuto kontrolu (-CheckOnly). Nikdy nerestartuje sám.
.PARAMETER Detailed
Rozšířený rozpis (jednotlivé certifikáty, posledních N událostí, boot manager chain).
.PARAMETER PassThru
Vrátí výsledný objekt do pipeline.
.PARAMETER LogPath
Cesta k logu. Default: log vedle skriptu vznikne jen při reálné remediaci.
.NOTES
Reference: KB5062710, KB5068202, KB5085046, KB5085790.
Vyžaduje Administrator (čtení UEFI databází, mount ESP, zápis registry).
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[switch]$CheckOnly,
[switch]$AssumeYes,
[switch]$Force,
[switch]$SkipScheduledTask,
[switch]$SkipBootManagerFileCheck,
[switch]$RegisterResume,
[switch]$Detailed,
[switch]$PassThru,
[string]$LogPath
)
$ErrorActionPreference = 'SilentlyContinue'
Set-StrictMode -Off
# ── Konzole: vynutit UTF-8 výstup (diakritika + symboly 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'
$REG_RUNONCE = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
$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
$WORK_ROOT = Join-Path $env:ProgramData 'SecureBootCA2023'
$STATE_FILE = Join-Path $WORK_ROOT 'state.json'
# Názvy certifikátů (pro X.509 i ASCII fallback detekci)
$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'
# Symboly checklistu (stavěné z code-pointů kvůli odolnosti vůči kódování; lze změnit zde)
$SYM_DONE = [string][char]0x2713 # ✓
$SYM_FAIL = [string][char]0x2717 # ✗
$SYM_PENDING = ' '
#endregion
#region ── Log / barevný výstup ───────────────────────────────────────────────
$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
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 {
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 =="
}
# Zvýrazněný řádek klíč → hodnota (hodnota barevně)
function Write-KV {
param([string]$Key, [string]$Value, [string]$ValueColor = 'White', [string]$Note, [string]$NoteColor = 'DarkGray')
Write-Host (" {0,-16}: " -f $Key) -ForegroundColor Gray -NoNewline
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)
}
# Řádek checklistu: [✓]/[ ]/[✗] + text + zvýrazněná poznámka
function Write-Check {
param(
[ValidateSet('Done','Pending','Fail','Info')][string]$State,
[string]$Text,
[string]$Note,
[string]$NoteColor = 'Yellow'
)
switch ($State) {
'Done' { $mark = "[$SYM_DONE]"; $markColor = 'Green'; $textColor = 'White' }
'Fail' { $mark = "[$SYM_FAIL]"; $markColor = 'Red'; $textColor = 'Red' }
'Pending' { $mark = "[$SYM_PENDING]"; $markColor = 'DarkGray'; $textColor = 'Gray' }
'Info' { $mark = "[$SYM_PENDING]"; $markColor = 'DarkGray'; $textColor = 'DarkGray' }
}
Write-Host (" {0} " -f $mark) -ForegroundColor $markColor -NoNewline
Write-Host $Text -ForegroundColor $textColor -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
$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 {
$r = Confirm-SecureBootUEFI -ErrorAction Stop
$state.IsSupported = $true; $state.IsEnabled = [bool]$r; $state.ConfirmResult = $r.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
}
#endregion
#region ── Detekce — certifikáty (X.509 parsing + ASCII fallback) ─────────────
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-SbVarAscii {
param([ValidateSet('PK','KEK','db','dbx')][string]$Name)
try {
$obj = Get-SecureBootUEFI -Name $Name -ErrorAction Stop
return [System.Text.Encoding]::ASCII.GetString($obj.Bytes)
} catch { return $null }
}
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 }
DbxRevokesPCA2011 = $false
AnyExpiring2011=$false
}
# KEK
try {
$kekObj = Get-SecureBootUEFI -Name KEK -ErrorAction Stop
foreach ($cert in (Parse-EFISignatureList -Bytes $kekObj.Bytes)) {
$subj = $cert.Subject; $info = Convert-CertToInfo $cert
if ($subj -like '*KEK CA 2011*') { $status.KEK.Has2011 = $true; $status.KEK.Certs2011 += $info }
if ($subj -like "*$CN_KEK2023*" -or $subj -like '*KEK*CA 2023*') { $status.KEK.Has2023 = $true; $status.KEK.Certs2023 += $info }
}
} catch { $status.KEK.Error = $_.Exception.Message }
# DB
try {
$dbObj = Get-SecureBootUEFI -Name db -ErrorAction Stop
foreach ($cert in (Parse-EFISignatureList -Bytes $dbObj.Bytes)) {
$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 "*$CN_UEFI2023*" -and $subj -notlike '*Option ROM*' -and $subj -notlike '*Windows UEFI*') { $status.DB.Has2023UEFI = $true; $status.DB.Certs2023 += $info }
if ($subj -like "*$CN_OPTROM2023*") { $status.DB.Has2023OptionROM = $true; $status.DB.Certs2023 += $info }
if ($subj -like "*$CN_WINUEFI2023*") { $status.DB.Has2023WindowsUEFI = $true; $status.DB.Certs2023 += $info }
}
} catch { $status.DB.Error = $_.Exception.Message }
# ASCII fallback (kdyby X.509 parse selhal) — jen doplní booleany, nikdy je neshazuje
$kekAscii = Get-SbVarAscii -Name KEK
if ($kekAscii) { if ($kekAscii -match [regex]::Escape($CN_KEK2023)) { $status.KEK.Has2023 = $true } }
$dbAscii = Get-SbVarAscii -Name db
if ($dbAscii) {
if ($dbAscii -match [regex]::Escape($CN_WINUEFI2023)) { $status.DB.Has2023WindowsUEFI = $true }
if ($dbAscii -match [regex]::Escape($CN_OPTROM2023)) { $status.DB.Has2023OptionROM = $true }
if ($dbAscii -match ('Microsoft UEFI CA 2023')) { $status.DB.Has2023UEFI = $true }
}
$dbxAscii = Get-SbVarAscii -Name dbx
if ($dbxAscii -and ($dbxAscii -match [regex]::Escape($CN_PCA2011))) { $status.DbxRevokesPCA2011 = $true }
$status.AnyExpiring2011 = $status.KEK.Has2011 -or $status.DB.Has2011UEFI -or $status.DB.Has2011WindowsPCA
return $status
}
#endregion
#region ── Detekce — registry / events / boot manager / task ──────────────────
function Get-RegistryStatus {
$reg = [ordered]@{
SecureBootEnabled=$null; AvailableUpdates=$null; HighConfidenceOptOut=$null
MicrosoftUpdateManagedOptIn=$null
ServicingKeyExists=$false; UEFICA2023Status=$null; UEFICA2023StatusText='KeyNotPresent'
UEFICA2023Error=$null; UEFICA2023ErrorEvent=$null; WindowsUEFICA2023Capable=$null
}
$mainProps = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue
if ($mainProps) {
$reg.SecureBootEnabled = $mainProps.SecureBootEnabled
$reg.AvailableUpdates = $mainProps.AvailableUpdates
$reg.HighConfidenceOptOut = $mainProps.HighConfidenceOptOut
$reg.MicrosoftUpdateManagedOptIn = $mainProps.MicrosoftUpdateManagedOptIn
}
$svcProps = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue
if ($svcProps) {
$reg.ServicingKeyExists = $true
$reg.UEFICA2023Status = $svcProps.UEFICA2023Status
$reg.UEFICA2023Error = $svcProps.UEFICA2023Error
$reg.UEFICA2023ErrorEvent = $svcProps.UEFICA2023ErrorEvent
$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-AvailableUpdatesText {
param($v)
if ($null -eq $v) { return '(nenastaveno)' }
$iv = [int]$v
$hex = '0x{0:X}' -f $iv
$note = switch ($iv) {
0 { 'vše aplikováno' }
0x4100 { 'KEK/DB hotové — restart pro Boot Manager' }
0x4000 { 'fáze Boot Manageru' }
0x5944 { 'naplánována plná sada' }
default { 'zbývá aplikovat' }
}
return "$hex ($note)"
}
function Get-EventLogStatus {
$evt = [ordered]@{ LastEventId=$null; LastEventTime=$null; ConfidenceLevel='NoRelevantEvents'
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) {
$e = $sorted | Where-Object { $_.Id -eq $id } | Select-Object -First 1
if ($e) { $evt.ById[$id] = [ordered]@{ Time=$e.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); Message=($e.Message -replace '\s+',' ').Trim() } }
}
foreach ($e in ($sorted | Select-Object -First 8)) {
$evt.RelevantEvents += [ordered]@{ EventId=$e.Id; TimeCreated=$e.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); Level=$e.LevelDisplayName }
}
$last = $sorted | Select-Object -First 1
$evt.LastEventId = $last.Id
$evt.LastEventTime = $last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
$evt.ConfidenceLevel = switch ($last.Id) {
1808 { 'HighConfidence-Success' } 1799 { 'BootManager-Installed' } 1801 { 'HighConfidence-Failed' }
1795 { 'Failed-FirmwareError' } 1796 { 'Failed-VariableUpdate' } 1803 { 'Blocked-NoOemPK' }
1802 { 'Pending' } default { 'Informational' } }
}
} catch {
if ($_.CategoryInfo.Reason -eq 'NoMatchingEventsException') { $evt.ConfidenceLevel = 'NoRelevantEvents' }
else { $evt.Error = $_.Exception.Message; $evt.ConfidenceLevel = 'EventLogError' }
}
return $evt
}
function Get-TaskExists {
try { $null = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop; return $true }
catch { return $false }
}
function Get-BootManagerStatus {
param([bool]$SkipFileCheck)
$r = [ordered]@{ HasCA2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error=$null; Checked=$false }
# Lehký signál: Event 1799 (Boot Manager 2023 nainstalován)
try {
$e = Get-WinEvent -FilterHashtable @{LogName='System';Id=1799} -MaxEvents 1 -ErrorAction Stop | Select-Object -First 1
if ($e -and ($e.Message -match 'Windows UEFI CA 2023')) { $r.HasCA2023 = $true; $r.Evidence += 'Event1799'; $r.Checked = $true }
} catch { }
if ($SkipFileCheck) { return $r }
# Robustní: mount ESP (read-only), certutil -dump bootmgfw.efi
$drive = 'S'; $mountedByUs = $false; $tmp = $null
try {
if (-not (Get-PSDrive -Name $drive -ErrorAction SilentlyContinue)) {
& mountvol "$drive`:" /S 2>$null | Out-Null
Start-Sleep -Seconds 2
$mountedByUs = $true
}
$src = "$drive`:\EFI\Microsoft\Boot\bootmgfw.efi"
if (Test-Path -LiteralPath $src) {
$r.Checked = $true
$tmp = Join-Path $env:TEMP ("bootmgfw_{0}.efi" -f ([guid]::NewGuid().ToString('N')))
Copy-Item -LiteralPath $src -Destination $tmp -Force -ErrorAction Stop
$certutil = Get-Command certutil.exe -ErrorAction SilentlyContinue
if ($certutil) {
$raw = & certutil.exe -dump $tmp 2>&1 | Out-String
if ($raw -match 'Windows UEFI CA 2023') {
$r.HasCA2023 = $true
if ($r.Evidence -notcontains 'certutil') { $r.Evidence += 'certutil' }
}
$issuer = ([regex]::Match($raw, '(?im)^\s*Issuer:\s*(.+)$')).Groups[1].Value.Trim()
if ($issuer) { $r.Chain = "Issuer: $issuer" }
}
try {
$sig = Get-AuthenticodeSignature -FilePath $tmp -ErrorAction Stop
if ($sig.SignerCertificate) { $r.Signer = $sig.SignerCertificate.Subject }
} catch { }
} else {
$r.Error = 'bootmgfw.efi nenalezen na ESP'
}
} catch {
$r.Error = $_.Exception.Message
} finally {
if ($tmp -and (Test-Path -LiteralPath $tmp)) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
if ($mountedByUs) { & mountvol "$drive`:" /D 2>$null | Out-Null }
}
return $r
}
#endregion
#region ── Fáze + kategorizace ────────────────────────────────────────────────
function Build-Phases {
param($Sb, $Cert, $Boot, $TaskExists, $Reg, $Evt)
$e1803 = $Evt.ById[1803]; $e1795 = $Evt.ById[1795]; $e1796 = $Evt.ById[1796]
$ph = [ordered]@{}
function _p($req,$done,$label) { return [ordered]@{ Req=$req; Done=[bool]$done; Label=$label; State=$null; Note=$null } }
$ph['SecureBootEnabled'] = _p $true $Sb.IsEnabled 'Secure Boot zapnutý'
$ph['TaskExists'] = _p $true $TaskExists 'Servicing task k dispozici'
$ph['Kek2023'] = _p $true $Cert.KEK.Has2023 "KEK: Microsoft Corporation $CN_KEK2023"
$ph['Db2023Windows'] = _p $true $Cert.DB.Has2023WindowsUEFI "DB: $CN_WINUEFI2023"
$ph['BootManager2023'] = _p $true $Boot.HasCA2023 'Boot Manager podepsaný Windows UEFI CA 2023'
$ph['Db2023ThirdParty'] = _p $false $Cert.DB.Has2023UEFI "DB: $CN_UEFI2023 (3rd-party, volitelné)"
$ph['Db2023OptionRom'] = _p $false $Cert.DB.Has2023OptionROM "DB: $CN_OPTROM2023 (volitelné)"
$ph['DbxRevoked'] = _p $false $Cert.DbxRevokesPCA2011 'DBX: revokace starého boot manageru 2011 (volitelné)'
# Stav + poznámky pro checklist
$kekDb = $ph['Kek2023'].Done -and $ph['Db2023Windows'].Done
foreach ($key in $ph.Keys) {
$p = $ph[$key]
if ($p.Done) { $p.State = 'Done'; continue }
if (-not $p.Req) { $p.State = 'Info'; continue }
# nesplněná povinná fáze — rozlišit blokované vs. čekající
switch ($key) {
'Kek2023' {
if ($e1803) { $p.State='Fail'; $p.Note='blokováno — chybí OEM PK-signed KEK (Event 1803)' }
elseif ($e1796) { $p.State='Fail'; $p.Note='selhání zápisu proměnné (Event 1796)' }
elseif ($e1795) { $p.State='Fail'; $p.Note='chyba firmwaru (Event 1795)' }
else { $p.State='Pending'; $p.Note='zbývá nasadit' }
}
'BootManager2023' {
if ($kekDb) { $p.State='Pending'; $p.Note="$([char]0x2190) zbývá — vyžaduje RESTART" }
else { $p.State='Pending'; $p.Note='až po nasazení KEK + DB' }
}
default {
if ($e1795) { $p.State='Fail'; $p.Note='chyba firmwaru (Event 1795)' }
else { $p.State='Pending'; $p.Note='zbývá nasadit' }
}
}
}
return $ph
}
function Get-RemediationCategory {
param($Result)
$sb = $Result.SecureBoot; $reg = $Result.Registry; $evt = $Result.EventLog
$env = $Result.EnvironmentType; $ph = $Result.Phases; $cert = $Result.Certificates
if (-not $sb.IsUEFI -or -not $sb.IsSupported) {
if ($env -like '*VM*') { return @{ Code='NO_SECUREBOOT_VM'; Tag='[X]'; Color='DarkGray'; Label='Secure Boot nepodporováno (VM bez vTPM/UEFI)' } }
return @{ Code='NO_SECUREBOOT'; Tag='[X]'; Color='DarkGray'; Label='Secure Boot nepodporováno (Legacy BIOS)' }
}
if (-not $sb.IsEnabled) { return @{ Code='SECUREBOOT_DISABLED'; Tag='[OFF]'; Color='Yellow'; Label='Secure Boot vypnuto' } }
if (-not $ph['TaskExists'].Done) {
return @{ Code='TASK_MISSING'; Tag='[!]'; Color='Magenta'; Label='Chybí servicing task — nainstalujte aktuální kumulativní update' }
}
if ($evt.ById[1803]) {
return @{ Code='KEK_BLOCKED'; Tag='[X]'; Color='Red'; Label='KEK nelze aktualizovat — chybí OEM PK-signed KEK (Event 1803)' }
}
if ($reg.UEFICA2023Status -eq 3 -or $evt.ById[1795] -or ($evt.ById[1796] -and -not $ph['Kek2023'].Done) -or $evt.LastEventId -eq 1801) {
return @{ Code='UPDATE_FAILED'; Tag='[FAIL]'; Color='Red'; Label='Selhání aktualizace certifikátů' }
}
$reqKeys = @('Kek2023','Db2023Windows','BootManager2023')
$missing = @($reqKeys | Where-Object { -not $ph[$_].Done })
$missingLabels = @($missing | ForEach-Object { $ph[$_].Label })
$anyApplied = $ph['Kek2023'].Done -or $ph['Db2023Windows'].Done -or $ph['BootManager2023'].Done -or $cert.DB.Has2023UEFI -or $cert.DB.Has2023OptionROM
if ($missing.Count -eq 0) {
if ($cert.AnyExpiring2011) {
return @{ Code='OK_TRANSITION'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — nové 2023 certifikáty i Boot Manager nasazeny (staré 2011 ještě přítomné, což je normální)' }
}
return @{ Code='OK'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — kompletní 2023 sada i Boot Manager, 2011 odstraněny' }
}
if ($missing.Count -eq 1 -and $missing[0] -eq 'BootManager2023') {
return @{ Code='UPDATE_PENDING'; Tag='[~]'; Color='Yellow'; Label='KEK i DB hotové — zbývá Boot Manager: vyžaduje RESTART' }
}
if ($null -ne $reg.WindowsUEFICA2023Capable -and $reg.WindowsUEFICA2023Capable -eq 0 -and -not $anyApplied) {
return @{ Code='FIRMWARE_UPDATE_NEEDED'; Tag='[FW]'; Color='Magenta'; Label='Firmware nepodporuje nové certifikáty — update u OEM' }
}
if ($anyApplied) {
return @{ Code='UPDATE_PARTIAL'; Tag='[~]'; Color='Cyan'; Label=('Probíhá po částech — zbývá: ' + ($missingLabels -join '; ')) }
}
return @{ Code='UPDATE_NEEDED'; Tag='[!]'; Color='Yellow'; Label='Nutná aktualizace certifikátů' }
}
#endregion
#region ── Detekce — orchestrace ──────────────────────────────────────────────
function Invoke-Detection {
param([bool]$SkipBootMgrFile)
$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$result = [ordered]@{
AuditTimestamp = (Get-Date).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
TaskExists = Get-TaskExists
BootManager = $null
Phases = $null
Category=$null; CategoryLabel=$null; CategoryColor=$null
}
if ($result.SecureBoot.IsUEFI -and $result.SecureBoot.IsSupported) {
$result.Certificates = Get-CertificateStatus
$result.BootManager = Get-BootManagerStatus -SkipFileCheck:$SkipBootMgrFile
} 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ý'}
DbxRevokesPCA2011=$false; AnyExpiring2011=$false }
$result.BootManager = [ordered]@{ HasCA2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error='Secure Boot nedostupný'; Checked=$false }
}
$result.Phases = Build-Phases -Sb $result.SecureBoot -Cert $result.Certificates -Boot $result.BootManager `
-TaskExists $result.TaskExists -Reg $result.Registry -Evt $result.EventLog
$cat = Get-RemediationCategory -Result $result
$result.Category = $cat.Code
$result.CategoryLabel = "$($cat.Tag) $($cat.Label)"
$result.CategoryColor = $cat.Color
return $result
}
#endregion
#region ── Výpis ──────────────────────────────────────────────────────────────
function Show-DetectionSummary {
param($R)
$sb = $R.SecureBoot; $c = $R.Certificates; $reg = $R.Registry; $evt = $R.EventLog; $hw = $R.Hardware; $ph = $R.Phases
Write-Head 'SERVER'
Write-KV 'Stroj' ("{0}" -f $R.Hostname) 'White' ("· {0} (build {1})" -f $R.OSCaption, $R.OSBuild)
Write-KV 'Prostředí' ("{0}" -f $R.EnvironmentType) 'White' ("· {0} {1}" -f $hw.Manufacturer, $hw.Model)
Write-Head 'POSTUP AKTUALIZACE (checklist)'
if ($sb.IsUEFI -and $sb.IsSupported) {
foreach ($key in $ph.Keys) {
$p = $ph[$key]
$label = $p.Label
if ($p.Done -and $p.State -eq 'Done' -and $key -eq 'Kek2023' -and $c.KEK.Certs2023.Count) { }
Write-Check -State $p.State -Text $label -Note $p.Note
}
} else {
Write-Check -State 'Fail' -Text 'Secure Boot není podporováno / zapnuto' -Note 'remediace certifikátů zde nedává smysl'
}
Write-Head 'STAV REGISTRŮ / FIRMWARE'
Write-KV 'AvailableUpdates' (Get-AvailableUpdatesText $reg.AvailableUpdates) 'Cyan'
Write-KV 'UEFICA2023Status' ("{0} ({1})" -f $reg.UEFICA2023Status, $reg.UEFICA2023StatusText) 'White'
if ($null -ne $reg.WindowsUEFICA2023Capable) { Write-KV 'FW 2023-capable' ([string]$reg.WindowsUEFICA2023Capable) $(if($reg.WindowsUEFICA2023Capable -eq 0){'Red'}else{'White'}) }
if ($null -ne $reg.UEFICA2023Error) { Write-KV 'UEFICA2023Error' ('0x{0:X}' -f [int]$reg.UEFICA2023Error) 'Red' }
if ($R.BootManager.Error) { Write-KV 'Boot Manager' 'neověřen souborově' 'Yellow' ("· {0}" -f $R.BootManager.Error) }
elseif ($R.BootManager.Evidence) { Write-KV 'Boot Manager' ('ověřeno ({0})' -f ($R.BootManager.Evidence -join ',')) $(if($R.BootManager.HasCA2023){'Green'}else{'Yellow'}) }
if ($evt.LastEventId) { Write-KV 'Poslední event' ("EventID {0}" -f $evt.LastEventId) $(if($evt.LastEventId -in @(1808,1799)){'Green'}elseif($evt.LastEventId -in @(1795,1796,1801,1803)){'Red'}else{'White'}) ("· {0}" -f $evt.LastEventTime) }
Write-Host ''
Write-Host ' VÝSLEDEK: ' -ForegroundColor Gray -NoNewline
Write-Host $R.CategoryLabel -ForegroundColor $R.CategoryColor
Add-LogLine ("VÝSLEDEK: {0}" -f $R.CategoryLabel)
}
function Show-DetailedDetection {
param($R)
$c = $R.Certificates; $evt = $R.EventLog; $bm = $R.BootManager
Write-Head 'PODROBNOSTI'
foreach ($grp in @(@{N='KEK';O=$c.KEK}, @{N='DB';O=$c.DB})) {
foreach ($ci in (@($grp.O.Certs2011) + @($grp.O.Certs2023))) {
Write-Host (" [{0}] {1}" -f $grp.N, $ci.Subject) -ForegroundColor DarkGray
Write-Host (" do {0} | {1}" -f $ci.NotAfter, $ci.Thumbprint) -ForegroundColor DarkGray
}
}
if ($bm.Chain) { Write-Host (" Boot Manager chain: {0}" -f $bm.Chain) -ForegroundColor DarkGray }
if ($bm.Signer) { Write-Host (" Boot Manager signer: {0}" -f $bm.Signer) -ForegroundColor DarkGray }
if ($evt.RelevantEvents.Count) {
Write-Host ' Poslední události:' -ForegroundColor DarkGray
foreach ($e in ($evt.RelevantEvents | Select-Object -First 6)) {
$ec = if ($e.EventId -in @(1808,1799)) { 'Green' } elseif ($e.EventId -in @(1795,1796,1801,1803)) { 'Red' } else { 'DarkGray' }
Write-Host (" [{0}] EventID {1} {2}" -f $e.TimeCreated, $e.EventId, $e.Level) -ForegroundColor $ec
}
}
}
#endregion
#region ── Plán remediace ─────────────────────────────────────────────────────
function Resolve-RemediationPlan {
param($R, [bool]$ForceMode)
$plan = [ordered]@{ Applicable=$false; AskUser=$false; Caution=$null; Reason=$null; NextSteps=@() }
switch -Wildcard ($R.Category) {
'OK' {
$plan.Reason='Server je kompletně hotový — žádná akce.'
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Už hotovo — -Force vynutí opětovné nastavení.' }
}
'OK_TRANSITION' {
$plan.Reason='Nové 2023 certifikáty i Boot Manager jsou nasazeny. Staré 2011 zůstávají (normální, neexpirují náhle). Žádná akce.'
$plan.NextSteps=@('Nepovinné: po čase ověřte revokaci starého boot manageru (DBX).')
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true }
}
'UPDATE_NEEDED' { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Reason='Lze zahájit aktualizaci metodou registry (KB5068202).' }
'UPDATE_PARTIAL' {
$plan.Applicable=$true; $plan.AskUser=$true
$plan.Reason='Část už nasazena, zbytek se aplikuje v dalším cyklu (po částech).'
$plan.Caution='Po nastavení a tasku bude potřeba RESTART; cyklus opakujte, dokud nebude HOTOVO.'
}
'UPDATE_PENDING' {
$plan.Applicable=$true; $plan.AskUser=$true
$plan.Reason='KEK i DB jsou hotové. Boot Manager se přepíše až po RESTARTU.'
$plan.Caution='Doporučeno: nech znovu nastavit registry/task a pak RESTARTUJ — tím se Boot Manager dokončí.'
$plan.NextSteps=@('Po tomto kroku RESTARTUJTE server.','Po restartu spusťte kontrolu znovu (nebo -RegisterResume).')
}
'UPDATE_FAILED' {
if ($R.EventLog.ById[1795]) {
$plan.Reason='Selhání s Event 1795 (chyba firmwaru / Hyper-V hostitele). Příčina je mimo tuto VM.'
$plan.NextSteps=@('Aktualizujte firmware / Hyper-V hostitele (KB5085790).','Poté spusťte kontrolu na VM znovu.')
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Event 1795 = problém firmwaru/hostitele; -Force jen zopakuje pokus.' }
} else {
$plan.Applicable=$true; $plan.AskUser=$true
$plan.Reason='Předchozí pokus selhal (Event 1801 / status Failed). Lze zopakovat.'
$plan.Caution='Zkontrolujte UEFICA2023Error výše před opakováním.'
}
}
'KEK_BLOCKED' {
$plan.Reason='KEK aktualizaci nelze vynutit — zařízení nemá OEM PK-signed KEK (Event 1803).'
$plan.NextSteps=@('Kontaktujte OEM / aktualizujte firmware.','Pokud není řešení, dokumentujte jako výjimku.')
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Blokováno firmwarem; -Force se pokusí i tak (pravděpodobně selže).' }
}
'TASK_MISSING' {
$plan.Reason='Servicing task \Microsoft\Windows\PI\Secure-Boot-Update neexistuje.'
$plan.NextSteps=@('Nainstalujte nejnovější kumulativní update Windows — task se vytvoří.','Poté spusťte kontrolu znovu.')
}
'FIRMWARE_UPDATE_NEEDED' {
$plan.Reason='WindowsUEFICA2023Capable=0 — firmware nemusí nové certifikáty podporovat.'
$plan.NextSteps=@('Aktualizujte firmware u výrobce (Dell/HP/Lenovo/Supermicro).','Pokud není dostupný, dokumentujte jako výjimku.')
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Firmware se jeví nezpůsobilý; -Force se pokusí i tak.' }
}
'SECUREBOOT_DISABLED' {
$plan.Reason='Secure Boot existuje, ale je vypnutý — zapnutí je rozhodnutí mimo rozsah skriptu.'
$plan.NextSteps=@('Zvažte zapnutí Secure Boot v UEFI/nastavení VM (pozor na BitLocker PCR7).')
}
'NO_SECUREBOOT*' {
$plan.Reason='Secure Boot není podporováno (Legacy BIOS / VM bez vTPM/UEFI).'
$plan.NextSteps=@('Dokumentujte jako výjimku.')
}
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 (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)$')
}
}
#endregion
#region ── Stav napříč restarty (state.json) + RunOnce resume ─────────────────
function Get-ScriptPath {
if ($PSCommandPath) { return $PSCommandPath }
if ($MyInvocation.MyCommand.Path) { return $MyInvocation.MyCommand.Path }
return $null
}
function Save-ResumeState {
param($R, [int]$Cycle)
try {
if (-not (Test-Path $WORK_ROOT)) { New-Item -Path $WORK_ROOT -ItemType Directory -Force | Out-Null }
@{ ComputerName=$env:COMPUTERNAME; Timestamp=(Get-Date).ToString('o'); Cycle=$Cycle
Category=$R.Category; AvailableUpdates=('0x{0:X}' -f [int]$R.Registry.AvailableUpdates) } |
ConvertTo-Json | Set-Content -LiteralPath $STATE_FILE -Encoding UTF8
} catch { }
}
function Get-ResumeState {
if (-not (Test-Path $STATE_FILE)) { return $null }
try { return (Get-Content -LiteralPath $STATE_FILE -Raw | ConvertFrom-Json) } catch { return $null }
}
function Clear-ResumeState { if (Test-Path $STATE_FILE) { Remove-Item -LiteralPath $STATE_FILE -Force -ErrorAction SilentlyContinue } }
function Register-Resume {
$sp = Get-ScriptPath
if (-not $sp) { Write-Line ' (Nelze zjistit cestu skriptu — auto-recheck nenastaven.)' DarkGray; return }
$cmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{0}" -CheckOnly' -f $sp
try {
if (-not (Test-Path $REG_RUNONCE)) { New-Item -Path $REG_RUNONCE -Force | Out-Null }
New-ItemProperty -Path $REG_RUNONCE -Name 'SecureBootCA2023Recheck' -Value $cmd -PropertyType String -Force | Out-Null
Write-Line ' Auto-recheck po příštím restartu nastaven (RunOnce, jen kontrola — žádný restart).' Green
} catch { Write-Line (" (Auto-recheck se nepodařilo nastavit: {0})" -f $_.Exception.Message) DarkGray }
}
#endregion
#region ── Remediace ──────────────────────────────────────────────────────────
function Invoke-Remediation {
param([bool]$SkipBootMgrFile)
$outcome = [ordered]@{ Status='Unknown'; Message=''; LogFile=$script:LogFile }
Write-Head 'REMEDIACE'
Add-LogLine 'REMEDIACE START'
# Krok 1/3 — registry (oficiální trigger) + ověření read-backem
Write-Host ' [1/3] Nastavuji registry (MicrosoftUpdateManagedOptIn=1, 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 'MicrosoftUpdateManagedOptIn' -Value 1 -Type DWord -Force -ErrorAction Stop
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 }
$verify = (Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction Stop).AvailableUpdates
if ($verify -ne $AVAILABLE_UPDATES_VALUE) { throw "Ověření selhalo — AvailableUpdates=$verify" }
Write-Host 'hotovo' -ForegroundColor Green
Add-LogLine 'Krok 1: MicrosoftUpdateManagedOptIn=1, AvailableUpdates=0x5944 zapsáno a ověřeno'
} catch {
Write-Host 'CHYBA' -ForegroundColor Red
Write-Line (" {0}" -f $_.Exception.Message) Red
$outcome.Status='Error'; $outcome.Message="Zápis registry selhal: $($_.Exception.Message)"
return $outcome
}
# Krok 2/3 — task + čekání na ZMĚNU AvailableUpdates (nebo doběh tasku)
if ($SkipScheduledTask) {
Write-Line ' [2/3] Servicing task přeskočen (-SkipScheduledTask) — spustí se sám (cca á 12 h).' DarkGray
} else {
Write-Host ' [2/3] Spouštím servicing task a čekám na změnu stavu ' -NoNewline
$task = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue
if (-not $task) {
Write-Host '— task nenalezen' -ForegroundColor Yellow
Write-Line ' Registry nastavena; Windows ji zpracuje při příštím servisním běhu.' DarkGray
} else {
$initial = [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
$now = [int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates)
if ($now -ne $initial) { $changed=$true; break }
} while ($state -eq 'Running' -and $elapsed -lt $TASK_TIMEOUT_SEC)
$now = [int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates)
if ($changed) { Write-Host (" hotovo ({0}s, AvailableUpdates -> 0x{1:X})" -f $elapsed, $now) -ForegroundColor Green }
elseif ($elapsed -ge $TASK_TIMEOUT_SEC) { Write-Host (" timeout {0}s (stav {1})" -f $elapsed,$state) -ForegroundColor Yellow }
else { Write-Host (" hotovo ({0}s, stav {1})" -f $elapsed,$state) -ForegroundColor Green }
Add-LogLine ("Krok 2: task stav=$state, elapsed=${elapsed}s, AvailableUpdates=0x{0:X}" -f $now)
} catch {
Write-Host '— nepodařilo se spustit' -ForegroundColor Yellow
Write-Line (" {0}" -f $_.Exception.Message) DarkGray
}
}
}
# Krok 3/3 — znovu detekce + aktualizovaný checklist
Write-Host ' [3/3] Ověřuji nový stav (vč. Boot Manageru) ... ' -NoNewline
Start-Sleep -Seconds 2
$after = Invoke-Detection -SkipBootMgrFile:$SkipBootMgrFile
Write-Host 'hotovo' -ForegroundColor Green
Write-Host ''
Write-Line (' AvailableUpdates: {0}' -f (Get-AvailableUpdatesText $after.Registry.AvailableUpdates)) Cyan
foreach ($key in @('Kek2023','Db2023Windows','BootManager2023')) {
$p = $after.Phases[$key]
Write-Check -State $p.State -Text $p.Label -Note $p.Note
}
$outcome.Status = 'Applied'
$outcome.After = $after
$outcome.Message = 'Registry nastavena, task zpracován. Boot Manager se přepíše až po restartu.'
Add-LogLine 'REMEDIACE KONEC: Applied'
return $outcome
}
#endregion
#region ── Main ───────────────────────────────────────────────────────────────
$isWhatIf = [bool]$WhatIfPreference
Write-Host ''
Write-Rule 'Cyan'
Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor White
Write-Host (" {0} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray
Write-Rule 'Cyan'
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
if (-not $isAdmin) { Write-Host ''; Write-Host ' ! Neběží jako Administrator — část kontrol a remediace nebude dostupná.' -ForegroundColor Yellow }
$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 }
Write-Host ''
Write-Host ' Zjišťuji stav (vč. ověření podpisu Boot Manageru)…' -ForegroundColor DarkGray
$result = Invoke-Detection -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent
Show-DetectionSummary -R $result
if ($Detailed) { Show-DetailedDetection -R $result }
# CheckOnly — jen detekce + exit kód
if ($CheckOnly) {
Write-Host ''
Write-Rule
$code = switch -Wildcard ($result.Category) {
'OK*' { 0 }
'KEK_BLOCKED' { 2 }
'FIRMWARE_UPDATE_NEEDED' { 2 }
'TASK_MISSING' { 2 }
'NO_SECUREBOOT*' { 2 }
'SECUREBOOT_DISABLED' { 2 }
'UPDATE_FAILED' { 2 }
default { 1 }
}
Write-Host (" CHECK: {0} (exit {1})" -f $result.CategoryLabel, $code) -ForegroundColor $result.CategoryColor
if ($PassThru) { $result }
exit $code
}
$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent
Write-Head 'VYHODNOCENÍ'
if ($plan.Reason) { Write-Host (" {0}" -f $plan.Reason) -ForegroundColor Gray }
$remediation = $null
if (-not $plan.Applicable) {
if ($plan.NextSteps.Count) {
Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White
$i=1; foreach ($s in $plan.NextSteps) { Write-Host (" {0}. {1}" -f $i,$s) -ForegroundColor Gray; $i++ }
} elseif ($result.Category -like 'OK*') {
Write-Host ' Není potřeba žádná akce.' -ForegroundColor Green
Clear-ResumeState
}
} else {
if ($plan.Caution) { Write-Host (" Pozor: {0}" -f $plan.Caution) -ForegroundColor Yellow }
if (-not $isAdmin) {
Write-Host ''; Write-Host ' Remediaci nelze provést bez práv Administrator.' -ForegroundColor Yellow
} elseif ($isWhatIf) {
Write-Host ''; Write-Host ' -WhatIf — co by remediace udělala (bez změn):' -ForegroundColor Cyan
Write-Host (" - MicrosoftUpdateManagedOptIn=1, AvailableUpdates=0x{0:X4}, HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE) -ForegroundColor Gray
if (-not $SkipScheduledTask) { Write-Host (" - spustila by servicing task a počkala na změnu stavu") -ForegroundColor Gray }
Write-Host ' - server by NErestartovala' -ForegroundColor Gray
} else {
$q = 'Chcete nyní zahájit / pokračovat v aktualizaci Secure Boot certifikátů?'
$d = 'Nastaví registry (KB5068202) a spustí servicing task. Server NEBUDE restartován.'
if (Get-UserConsent -Question $q -Detail $d) {
$script:LogActive = $true
$cycle = if ($prev) { [int]$prev.Cycle + 1 } else { 1 }
Add-LogLine ("Cyklus #{0} | {1} | kategorie={2}" -f $cycle, $result.Hostname, $result.Category)
$remediation = Invoke-Remediation -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent
if ($remediation.After) { Save-ResumeState -R $remediation.After -Cycle $cycle }
} else {
Write-Host ''; Write-Host ' Remediace neprovedena (volba uživatele).' -ForegroundColor Yellow
}
}
}
# Závěr
Write-Host ''
Write-Rule 'Cyan'
if ($remediation -and $remediation.Status -eq 'Applied') {
$after = $remediation.After
if ($after.Category -like 'OK*') {
Write-Host (" {0} HOTOVO — server je kompletní." -f $SYM_DONE) -ForegroundColor Green
Clear-ResumeState
} else {
Write-Host ' ČÁSTEČNĚ HOTOVO — proces pokračuje po restartu.' -ForegroundColor Yellow
Write-Host ''
Write-Host ' Další kroky:' -ForegroundColor White
Write-Host ' 1. Naplánujte RESTART serveru (skript jej záměrně neprovedl).' -ForegroundColor Gray
Write-Host ' 2. BitLocker s PCR7: před restartem ověřte recovery key.' -ForegroundColor Gray
Write-Host ' 3. Po restartu spusťte kontrolu znovu — dokud nebude zelené HOTOVO.' -ForegroundColor Gray
Write-Host (' AvailableUpdates musí klesat (0x5944 -> 0x4100 -> 0x4000 -> 0x0).') -ForegroundColor DarkGray
if ($RegisterResume) { Register-Resume }
else { Write-Host ' (Tip: -RegisterResume spustí kontrolu po příštím restartu automaticky.)' -ForegroundColor DarkGray }
}
Write-Host ''
Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray
} elseif ($remediation -and $remediation.Status -eq 'Error') {
Write-Host (' CHYBA REMEDIACE — {0}' -f $remediation.Message) -ForegroundColor Red
} else {
Write-Host ' KONEC — bez změn na serveru.' -ForegroundColor Cyan
}
Write-Rule 'Cyan'
Write-Host ''
if ($PassThru) { return [pscustomobject]@{ Detection=$result; Plan=$plan; Remediation=$remediation } }
#endregion