From d6cefff29bfcbc673e070023eec82b63e9e93f78 Mon Sep 17 00:00:00 2001 From: Petr Stepan Date: Fri, 5 Jun 2026 18:23:36 +0200 Subject: [PATCH] =?UTF-8?q?Zapracovani=20zlepsen=C3=AD=20z=20Filipova=20Sc?= =?UTF-8?q?riptu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 8196 -> 8196 bytes Invoke-SecureBootRemediation.ps1 | 856 ++++++++++++++++++++----------- 2 files changed, 562 insertions(+), 294 deletions(-) diff --git a/.DS_Store b/.DS_Store index 7f9348d90648d5cc0dd9c9f3428dde44ed0c16a4..3c09b4dc2c57986ac7d019435fcd2b72367e211a 100644 GIT binary patch delta 14 VcmZp1XmQx^Nq~`I^Jf7;egG>A1rh)N delta 14 VcmZp1XmQx^Nq~`Y^Jf7;egG>G1rq=O diff --git a/Invoke-SecureBootRemediation.ps1 b/Invoke-SecureBootRemediation.ps1 index 854d5a7..b941287 100644 --- a/Invoke-SecureBootRemediation.ps1 +++ b/Invoke-SecureBootRemediation.ps1 @@ -4,68 +4,67 @@ 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: + 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"). - 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. + 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. Restart je nutný pro dokončení aktualizace, - ale necháváme ho na plánovaném okně správce. + 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. - 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 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á). - Vhodné pro neinteraktivní běh. Restart se stále NEPROVÁDÍ. + 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ý, již úspěšně dokončeno, Hyper-V Event 1795). + 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. Task se spustí sám - při příštím plánovaném běhu (cca každých 12 h). + 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 - Vypíše rozšířený detekční rozpis (jednotlivé certifikáty, posledních N událostí). + Rozšířený rozpis (jednotlivé certifikáty, posledních N událostí, boot manager chain). .PARAMETER PassThru - Vrátí výsledný objekt (detekce + případná remediace) do pipeline. + Vrátí výsledný objekt 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). + Cesta k logu. Default: log vedle skriptu vznikne jen při reálné remediaci. .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. + 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 @@ -74,9 +73,7 @@ param( $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. +# ── 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 @@ -87,20 +84,35 @@ try { $REG_SECUREBOOT = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot' $REG_SERVICING = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' +$REG_RUNONCE = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' $AVAILABLE_UPDATES_VALUE = 0x5944 # 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 / výpis ──────────────────────────────────────────────────────── +#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 # při explicitním -LogPath logujeme od začátku +$script:LogActive = [bool]$LogPath function Add-LogLine { param([string]$Text) @@ -110,17 +122,51 @@ function Add-LogLine { } 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 } + Write-Host $Text -ForegroundColor $Color if (-not $NoLog) { Add-LogLine $Text } } -function Write-Rule { Write-Host ('=' * 62) -ForegroundColor DarkCyan } +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 (z Invoke-SecureBootAudit.ps1) ───────────────────────────── +#region ── Detekce — prostředí / HW / Secure Boot ───────────────────────────── function Get-EnvironmentType { try { @@ -161,8 +207,8 @@ function Get-SecureBootState { $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() + $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*') { @@ -176,6 +222,10 @@ function Get-SecureBootState { return $state } +#endregion + +#region ── Detekce — certifikáty (X.509 parsing + ASCII fallback) ───────────── + function Parse-EFISignatureList { param([byte[]]$Bytes) $certs = @() @@ -214,57 +264,86 @@ function Convert-CertToInfo { 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 } - AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false + DbxRevokesPCA2011 = $false + AnyExpiring2011=$false } + + # KEK try { $kekObj = Get-SecureBootUEFI -Name KEK -ErrorAction Stop - $kekCerts = Parse-EFISignatureList -Bytes $kekObj.Bytes - foreach ($cert in $kekCerts) { + 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 '*KEK 2K CA 2023*' -or ($subj -like '*KEK*CA 2023*')) { $status.KEK.Has2023 = $true; $status.KEK.Certs2023 += $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 - $dbCerts = Parse-EFISignatureList -Bytes $dbObj.Bytes - foreach ($cert in $dbCerts) { + $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 '*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 } + 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 } - $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 + + # 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; WindowsUEFICA2023Capable=$null + 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.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' } @@ -274,67 +353,207 @@ function Get-RegistryStatus { 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 { - $evtStatus = [ordered]@{ LastEventId=$null; LastEventTime=$null; ConfidenceLevel='NoRelevantEvents'; RelevantEvents=@(); Error=$null } - $relevantIds = @(1795,1796,1800,1801,1802,1803,1808) + $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=$relevantIds } -MaxEvents 20 -ErrorAction Stop + $events = Get-WinEvent -FilterHashtable @{ LogName='System'; Id=$ids } -MaxEvents 40 -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 } + 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 - $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' } } + $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') { $evtStatus.ConfidenceLevel = 'NoRelevantEvents' } - else { $evtStatus.Error = $_.Exception.Message; $evtStatus.ConfidenceLevel = 'EventLogError' } + if ($_.CategoryInfo.Reason -eq 'NoMatchingEventsException') { $evt.ConfidenceLevel = 'NoRelevantEvents' } + else { $evt.Error = $_.Exception.Message; $evt.ConfidenceLevel = 'EventLogError' } } - return $evtStatus + 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; $cert = $Result.Certificates; $reg = $Result.Registry; $env = $Result.EnvironmentType + $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'; 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 ($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'; 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 (-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 ($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 ($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 $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 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ů' } } - if ($reg.UEFICA2023Status -eq 1 -or $reg.UEFICA2023Status -eq 2) { - return @{ Code='UPDATE_PENDING'; Emoji='[WAIT]'; Label='Aktualizace připravena, čeká na restart' } + + $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 ($null -ne $reg.WindowsUEFICA2023Capable -and $reg.WindowsUEFICA2023Capable -eq 0) { - return @{ Code='FIRMWARE_UPDATE_NEEDED'; Emoji='[FW]'; Label='Čeká na firmware update (OEM)' } + 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' } } - return @{ Code='UPDATE_NEEDED'; Emoji='[!]'; Label='Nutná aktualizace certifikátů' } + 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 { - $auditStart = Get-Date - $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue + param([bool]$SkipBootMgrFile) + $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $result = [ordered]@{ - AuditTimestamp = $auditStart.ToString('yyyy-MM-dd HH:mm:ss') + 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 } @@ -344,140 +563,154 @@ function Invoke-Detection { Certificates = $null Registry = Get-RegistryStatus EventLog = Get-EventLogStatus - Category = $null - CategoryLabel = $null + 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ý' } - AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false } + KEK=[ordered]@{Has2011=$false;Has2023=$false;Certs2011=@();Certs2023=@();Error='Secure Boot nedostupný'} + DB =[ordered]@{Has2011UEFI=$false;Has2011WindowsPCA=$false;Has2023UEFI=$false;Has2023OptionROM=$false;Has2023WindowsUEFI=$false;Certs2011=@();Certs2023=@();Error='Secure Boot nedostupný'} + DbxRevokesPCA2011=$false; AnyExpiring2011=$false } + $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.Emoji) $($cat.Label)" + $result.CategoryLabel = "$($cat.Tag) $($cat.Label)" + $result.CategoryColor = $cat.Color return $result } #endregion -#region ── Stručný výpis detekce ────────────────────────────────────────────── +#region ── Výpis ────────────────────────────────────────────────────────────── function Show-DetectionSummary { param($R) - $sb = $R.SecureBoot; $c = $R.Certificates; $reg = $R.Registry; $evt = $R.EventLog; $hw = $R.Hardware + $sb = $R.SecureBoot; $c = $R.Certificates; $reg = $R.Registry; $evt = $R.EventLog; $hw = $R.Hardware; $ph = $R.Phases - 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) + 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) { - $l2011 = @() - if ($c.KEK.Has2011) { $l2011 += 'KEK CA 2011' } - if ($c.DB.Has2011UEFI) { $l2011 += 'UEFI CA 2011' } - if ($c.DB.Has2011WindowsPCA) { $l2011 += 'Windows PCA 2011' } - $l2023 = @() - if ($c.KEK.Has2023) { $l2023 += 'KEK 2K CA 2023' } - if ($c.DB.Has2023UEFI) { $l2023 += 'UEFI CA 2023' } - if ($c.DB.Has2023OptionROM) { $l2023 += 'Option ROM 2023' } - if ($c.DB.Has2023WindowsUEFI) { $l2023 += 'Windows UEFI CA 2023' } - - $t2011 = if ($l2011.Count) { ('přítomny ({0}) — expirují 6–10/2026' -f ($l2011 -join ', ')) } else { 'nepřítomny' } - $t2023 = if ($l2023.Count) { ('přítomny ({0})' -f ($l2023 -join ', ')) } else { 'CHYBÍ' } - Write-Line (" Certifikáty 11 : {0}" -f $t2011) - Write-Line (" Certifikáty 23 : {0}" -f $t2023) - Write-Line (" Registry stav : UEFICA2023Status = {0} ({1})" -f $reg.UEFICA2023Status, $reg.UEFICA2023StatusText) - if ($null -ne $reg.UEFICA2023Error) { Write-Line (" UEFICA2023Error = {0}" -f $reg.UEFICA2023Error) Red } - if ($evt.LastEventId) { Write-Line (" Poslední event : EventID {0} ({1})" -f $evt.LastEventId, $evt.LastEventTime) } + 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' } - $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 + 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 - Write-Line '' - Write-Line 'PODROBNOSTI' DarkYellow + $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})) { - $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 + 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-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 + 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 ── Vyhodnocení remediace ────────────────────────────────────────────── +#region ── Plán 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í.' } + $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 = '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í.' } + $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' { + '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='Lze aplikovat remediaci metodou registry (KB5068202).' + $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.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.') + $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 ($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.' } + 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 (EventID 1801 / status Failed). Lze zopakovat metodou registry.' - $plan.Caution='Před opakováním zkontrolujte UEFICA2023Error (viz detekce výše).' + $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 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).' } + $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 tohoto skriptu.' - $plan.NextSteps=@('Rozhodněte o zapnutí Secure Boot v UEFI/nastavení VM (pozor na BitLocker PCR7).') + $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 nebo VM bez vTPM/UEFI).' - $plan.NextSteps=@('Dokumentujte server jako výjimku — remediace certifikátů zde nedává smysl.') + $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.' } } @@ -486,115 +719,134 @@ function Resolve-RemediationPlan { 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 - } + 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) + $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 { - $ans = Read-Host 'Aplikovat? [a/N]' - return ($ans -match '^(a|ano|y|yes)$') + return ((Read-Host 'Aplikovat? [a/N]') -match '^(a|ano|y|yes)$') } } #endregion -#region ── Remediace (sekvenčně, s čekáním; z Set-SecureBootCertificateUpdate.ps1) ── +#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 { - <# 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 + param([bool]$SkipBootMgrFile) + $outcome = [ordered]@{ Status='Unknown'; Message=''; LogFile=$script:LogFile } + Write-Head 'REMEDIACE' 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 + # 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 ($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)" } - + if ($verify -ne $AVAILABLE_UPDATES_VALUE) { throw "Ověření selhalo — AvailableUpdates=$verify" } Write-Host 'hotovo' -ForegroundColor Green - Add-LogLine ("Krok 1: AvailableUpdates=0x{0:X4} zapsáno a ověřeno; HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE) + 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)" - Add-LogLine "Krok 1 CHYBA: $($_.Exception.Message)" return $outcome } - # ── Krok 2/3 — servicing task a ČEKÁNÍ na dokončení ── + # 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 - Add-LogLine 'Krok 2: task přeskočen (-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 dokončení ' -NoNewline + 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 ' 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ě' + 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 # dát tasku čas přejít do stavu Running - $elapsed = 0; $state = 'Running' + 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) - - $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" - } + $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 - 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 + # Krok 3/3 — znovu detekce + aktualizovaný checklist + Write-Host ' [3/3] Ověřuji nový stav (vč. Boot Manageru) ... ' -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 } - + $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.Message = 'Registry nastavena a ověřena, servicing task zpracován. Certifikáty se aplikují až při restartu.' + $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 } @@ -606,93 +858,109 @@ function Invoke-Remediation { $isWhatIf = [bool]$WhatIfPreference Write-Host '' -Write-Rule -Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor Cyan +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 +Write-Rule 'Cyan' -# 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 -} +if (-not $isAdmin) { Write-Host ''; Write-Host ' ! Neběží jako Administrator — část kontrol a remediace nebude dostupná.' -ForegroundColor Yellow } -# ── 1) DETEKCE ── -$result = Invoke-Detection +$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 } -# ── 2) VYHODNOCENÍ ── -$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent +# 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 +} -Write-Line '' -Write-Line 'VYHODNOCENÍ' Yellow -if ($plan.Reason) { Write-Line (" {0}" -f $plan.Reason) } +$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) { - # 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 + 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-Line (" Pozor: {0}" -f $plan.Caution) Yellow } - + if ($plan.Caution) { Write-Host (" Pozor: {0}" -f $plan.Caution) -ForegroundColor Yellow } if (-not $isAdmin) { - Write-Line '' - Write-Line ' Remediaci nelze provést bez práv Administrator. Spusťte skript elevovaně.' Yellow + Write-Host ''; Write-Host ' Remediaci nelze provést bez práv Administrator.' -ForegroundColor 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' + 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 { - # ── 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.' + $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 # od teď logujeme do souboru - Add-LogLine ("Detekce: {0} | kategorie={1}" -f $result.Hostname, $result.Category) - $remediation = Invoke-Remediation + $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-Line '' - Write-Line ' Remediace neprovedena (volba uživatele). Žádné změny.' Yellow + Write-Host ''; Write-Host ' Remediace neprovedena (volba uživatele).' -ForegroundColor Yellow } } } -# ── 4) ZÁVĚR / DALŠÍ KROKY ── -Write-Line '' -Write-Rule +# Závěr +Write-Host '' +Write-Rule 'Cyan' 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 + $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-Line ('CHYBA REMEDIACE — {0}' -f $remediation.Message) Red - Write-Line ("Log: {0}" -f $script:LogFile) DarkGray + Write-Host (' CHYBA REMEDIACE — {0}' -f $remediation.Message) -ForegroundColor Red } else { - Write-Line 'KONEC — bez změn na serveru.' Cyan + Write-Host ' KONEC — bez změn na serveru.' -ForegroundColor Cyan } -Write-Rule +Write-Rule 'Cyan' Write-Host '' -if ($PassThru) { - return [pscustomobject]@{ Detection = $result; Plan = $plan; Remediation = $remediation } -} +if ($PassThru) { return [pscustomobject]@{ Detection=$result; Plan=$plan; Remediation=$remediation } } #endregion