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