1134 lines
65 KiB
PowerShell
1134 lines
65 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Detekce + interaktivní remediace Secure Boot certifikátů na jednom serveru (KB5062710 / KB5068202).
|
|
|
|
.DESCRIPTION
|
|
Sloučení detekčního a remediačního skriptu do jednoho lokálního průvodce s fázovým
|
|
checklistem. Server prohlásí za HOTOVÝ teprve když jsou splněné VŠECHNY povinné fáze —
|
|
včetně toho, že se systém reálně bootuje z Boot Manageru podepsaného Windows UEFI CA 2023
|
|
(signalizuje WindowsUEFICA2023Capable=2 / Event 1808 / 1799).
|
|
|
|
Respektuje oficiální MS guidance (autoritativní hodnoty):
|
|
- UEFICA2023Status (REG_SZ): NotStarted / InProgress / Updated
|
|
- WindowsUEFICA2023Capable: 0=cert není v DB, 1=cert v DB, 2=bootuje se z 2023 boot manageru
|
|
- ConfidenceLevel (REG_SZ): High Confidence / Temporarily Paused / Not Supported / ...
|
|
- AvailableUpdates: 0x5944 -> 0x5904 -> 0x5104 -> 0x4104 -> 0x4100 -> 0x4000 -> 0x0
|
|
- Cílový stav: AvailableUpdates=0x0, UEFICA2023Status=Updated, WindowsUEFICA2023Capable=2
|
|
- Events (TPM-WMI): 1795=chyba firmwaru, 1796=selhání zápisu UEFI var,
|
|
1802=pozastaveno (firmware), 1803=odloženo (monitor), 1801=čeká (monitor)
|
|
|
|
Skript nabídne restart (interaktivní dotaz) jen při AvailableUpdates=0x4100 (Boot Manager
|
|
staged). Ve všech ostatních stavech task spustí a posune hodnotu. Sleduje stav napříč
|
|
restarty (state.json). Volitelně registruje RunOnce pro auto-check po restartu (-RegisterResume).
|
|
|
|
.PARAMETER CheckOnly
|
|
Jen detekce + checklist + exit kód (0=hotovo, 1=nutná akce, 2=blokováno). Bez změn/dotazu.
|
|
|
|
.PARAMETER AssumeYes
|
|
Přeskočí dotaz a remediaci rovnou aplikuje (pokud je smysluplná). Bez restartu.
|
|
|
|
.PARAMETER Force
|
|
Aplikuje remediaci i ve stavech, kdy ji skript jinak nedoporučuje.
|
|
|
|
.PARAMETER SkipScheduledTask
|
|
Nastaví registry, ale nespustí servicing task (spustí se sám cca á 12 h).
|
|
|
|
.PARAMETER SkipBootManagerFileCheck
|
|
Neověřuje bootmgfw.efi na ESP přes mount+certutil. Dokončení se pozná z
|
|
WindowsUEFICA2023Capable=2 a UEFICA2023Status=Updated.
|
|
|
|
.PARAMETER RegisterResume
|
|
Po remediaci, kde zbývá restart, zaregistruje RunOnce, který po PŘÍŠTÍM (ručním) restartu
|
|
automaticky spustí kontrolu (-CheckOnly). Nikdy nerestartuje sám.
|
|
|
|
.PARAMETER Detailed
|
|
Rozšířený rozpis (certifikáty, události, boot manager chain, bit-breakdown AvailableUpdates).
|
|
|
|
.PARAMETER PassThru
|
|
Vrátí výsledný objekt do pipeline.
|
|
|
|
.PARAMETER LogPath
|
|
Cesta k logu. Default: log vedle skriptu vznikne jen při reálné remediaci.
|
|
|
|
.NOTES
|
|
Reference: KB5062710, KB5068202, KB5066835 (min. build 10/2025), KB5085790.
|
|
Vyžaduje Administrator (čtení UEFI databází, mount ESP, zápis registry).
|
|
#>
|
|
|
|
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
|
|
param(
|
|
[switch]$CheckOnly,
|
|
[switch]$AssumeYes,
|
|
[switch]$Force,
|
|
[switch]$SkipScheduledTask,
|
|
[switch]$SkipBootManagerFileCheck,
|
|
[switch]$RegisterResume,
|
|
[switch]$Detailed,
|
|
[switch]$PassThru,
|
|
[string]$LogPath
|
|
)
|
|
|
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
Set-StrictMode -Off
|
|
|
|
# ── Konzole: vynutit UTF-8 výstup (diakritika + symboly i ve Windows PowerShell 5.1) ──
|
|
try {
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[Console]::OutputEncoding = $utf8NoBom
|
|
$OutputEncoding = $utf8NoBom
|
|
} catch { }
|
|
|
|
#region ── Konstanty ──────────────────────────────────────────────────────────
|
|
|
|
$REG_SECUREBOOT = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot'
|
|
$REG_SERVICING = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing'
|
|
$REG_RUNONCE = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
|
$AVAILABLE_UPDATES_VALUE = 0x5944
|
|
$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'
|
|
|
|
# Bity AvailableUpdates (dle CheckCA2023 / MS) — pro -Detailed rozpis
|
|
$AU_BITS = @(
|
|
@{ Bit=0x0002; Name='DBX revocation update' }
|
|
@{ Bit=0x0004; Name='KEK 2K CA 2023 -> KEK' }
|
|
@{ Bit=0x0020; Name='SkuSiPolicy update' }
|
|
@{ Bit=0x0040; Name='Windows UEFI CA 2023 -> DB' }
|
|
@{ Bit=0x0080; Name='PCA 2011 -> DBX (revoke old boot mgr)' }
|
|
@{ Bit=0x0100; Name='Boot manager update' }
|
|
@{ Bit=0x0200; Name='SVN firmware update' }
|
|
@{ Bit=0x0400; Name='SBAT firmware update' }
|
|
@{ Bit=0x0800; Name='Option ROM UEFI CA 2023 -> DB' }
|
|
@{ Bit=0x1000; Name='Microsoft UEFI CA 2023 -> DB' }
|
|
@{ Bit=0x4000; Name='Conditional CA 2023 guard bit' }
|
|
)
|
|
|
|
# Značky checklistu. ASCII kvůli kompatibilitě — font Consolas (default konzole) nemá blok
|
|
# Dingbats, takže ✓/✗ by se zobrazily jako □. Chcete-li v Consolas „fajfku", nastavte
|
|
# $SYM_DONE = [string][char]0x221A (√, blok Mathematical Operators, Consolas ho má).
|
|
$SYM_DONE = '+'
|
|
$SYM_FAIL = '!'
|
|
$SYM_PENDING = ' '
|
|
|
|
#endregion
|
|
|
|
#region ── Log / barevný výstup ───────────────────────────────────────────────
|
|
|
|
# Jeden společný log na LOKÁLNÍM disku serveru (%ProgramData%), připojovaný pod sebe přes běhy.
|
|
# Záměrně NE vedle skriptu — ten může být RDP-redirected disk, kde per-řádkový zápis vytváří
|
|
# poškozené (null) soubory. Řádky se bufferují a zapíšou jednorázově (Flush-Log), což je odolné.
|
|
$script:LogFile = if ($LogPath) { $LogPath } else { 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,-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)
|
|
}
|
|
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="[$SYM_PENDING]"; $mc='DarkGray'; $tc='Gray' }
|
|
'Info' { $mark="[$SYM_PENDING]"; $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 / mode / BitLocker ──────────
|
|
|
|
function Get-EnvironmentType {
|
|
try {
|
|
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
|
$mfr=[string]$cs.Manufacturer; $model=[string]$cs.Model
|
|
if ($mfr -like '*VMware*') { return 'VMware VM' }
|
|
if ($mfr -like '*Microsoft*' -and $model -eq 'Virtual Machine') { return 'Hyper-V VM' }
|
|
if ($model -like '*Virtual*' -or $mfr -like '*QEMU*' -or $mfr -like '*Xen*') { return 'Other VM' }
|
|
return 'Physical'
|
|
} catch { return 'Unknown' }
|
|
}
|
|
|
|
function Get-HardwareInfo {
|
|
$hw = [ordered]@{ Manufacturer='Unknown'; Model='Unknown'; 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 {
|
|
# User / Deployed / Setup / Audit ze standardních UEFI proměnných
|
|
$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 / events / boot manager ───────────
|
|
|
|
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, v každé seznam SignatureEntry s 16B GUID prefixem.
|
|
param([byte[]]$Bytes)
|
|
$certs = @()
|
|
if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs }
|
|
|
|
# GUID pro EFI_CERT_X509_GUID: {a5c059a1-94e4-4aa7-87b5-ab155c2bf072} (little-endian bytes)
|
|
$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 }
|
|
|
|
# Zpracovat pouze X.509 záznamy
|
|
$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 (kdyby X.509 parse selhal) — jen doplní booleany
|
|
$k=Get-SbVarAscii KEK; if ($k -and $k -match [regex]::Escape($CN_KEK2023)) { $st.KEK.Has2023=$true }
|
|
$d=Get-SbVarAscii db
|
|
if ($d) {
|
|
if ($d -match [regex]::Escape($CN_WINUEFI2023)) { $st.DB.Has2023WindowsUEFI=$true }
|
|
if ($d -match [regex]::Escape($CN_OPTROM2023)) { $st.DB.Has2023OptionROM=$true }
|
|
if ($d -match 'Microsoft UEFI CA 2023') { $st.DB.Has2023UEFI=$true }
|
|
}
|
|
$x=Get-SbVarAscii dbx; if ($x -and $x -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
|
|
}
|
|
$m = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue
|
|
if ($m) {
|
|
$reg.AvailableUpdates=$m.AvailableUpdates; $reg.HighConfidenceOptOut=$m.HighConfidenceOptOut
|
|
$reg.MicrosoftUpdateManagedOptIn=$m.MicrosoftUpdateManagedOptIn
|
|
}
|
|
$s = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue
|
|
if ($s) {
|
|
$reg.ServicingKeyExists=$true
|
|
$reg.UEFICA2023Status=$s.UEFICA2023Status
|
|
$reg.UEFICA2023Error=$s.UEFICA2023Error
|
|
$reg.UEFICA2023ErrorEvent=$s.UEFICA2023ErrorEvent
|
|
$reg.WindowsUEFICA2023Capable=$s.WindowsUEFICA2023Capable
|
|
$reg.ConfidenceLevel=$s.ConfidenceLevel
|
|
}
|
|
# UEFICA2023Status je REG_SZ (NotStarted/InProgress/Updated); defenzivně i číselná varianta
|
|
$v=$reg.UEFICA2023Status
|
|
if ($null -eq $v) { $reg.UEFICA2023StatusText='KeyNotPresent' }
|
|
elseif ($v -is [string] -and $v -ne '') { $reg.UEFICA2023StatusText=$v }
|
|
else { $reg.UEFICA2023StatusText = switch ([int]$v) { 0{'NotStarted'} 1{'InProgress'} 2{'Updated'} 3{'Failed'} default {"($v)"} } }
|
|
# WindowsUEFICA2023Capable: 0/1/2
|
|
$reg.WindowsUEFICA2023CapableText = switch ($reg.WindowsUEFICA2023Capable) {
|
|
0 {'0 = Windows UEFI CA 2023 NENÍ v DB'} 1 {'1 = cert v DB (boot mgr 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
|
|
$n = 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á na 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 {'zbývá aplikovat'} }
|
|
return "$hex ($n)"
|
|
}
|
|
|
|
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
|
|
# Index posledního výskytu každého ID
|
|
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') } }
|
|
}
|
|
# Posledních 8 událostí pro -Detailed výpis
|
|
foreach ($ev in ($sorted | Select-Object -First 8)) {
|
|
$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 }
|
|
}
|
|
|
|
function Get-BootManagerStatus {
|
|
# Ověří, zda je bootmgfw.efi na ESP podepsaný Windows UEFI CA 2023 (staged stav).
|
|
param([bool]$SkipFileCheck)
|
|
$r=[ordered]@{ EspHas2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error=$null; Checked=$false }
|
|
if ($SkipFileCheck) { return $r }
|
|
$drive='S'; $mounted=$false; $tmp=$null
|
|
try {
|
|
if (-not (Get-PSDrive -Name $drive -ErrorAction SilentlyContinue)) { & mountvol "$drive`:" /S 2>$null | Out-Null; Start-Sleep -Seconds 2; $mounted=$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
|
|
if (Get-Command certutil.exe -ErrorAction SilentlyContinue) {
|
|
$raw = & certutil.exe -dump $tmp 2>&1 | Out-String
|
|
if ($raw -match 'Windows UEFI CA 2023') { $r.EspHas2023=$true; $r.Evidence+='certutil' }
|
|
$iss=([regex]::Match($raw,'(?im)^\s*Issuer:\s*(.+)$')).Groups[1].Value.Trim(); if ($iss){ $r.Chain="Issuer: $iss" }
|
|
}
|
|
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 ($mounted) { & mountvol "$drive`:" /D 2>$null | Out-Null }
|
|
}
|
|
return $r
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Fáze + kategorizace ────────────────────────────────────────────────
|
|
|
|
function Build-Phases {
|
|
param($Sb, $Cert, $Boot, $TaskExists, $Reg, $Evt)
|
|
$e1795=$Evt.ById[1795]; $e1796=$Evt.ById[1796]
|
|
$cap=$Reg.WindowsUEFICA2023Capable
|
|
# Pouze Capable=2 je autoritativní signál aktivního Boot Manageru 2023.
|
|
# Eventy 1808/1799 nejsou Done signálem — mohou být ze starých/předchozích běhů a jsou nespolehlivé.
|
|
$bmActive = ($cap -eq 2)
|
|
$bmStaged = [bool]$Boot.EspHas2023
|
|
# UEFICA2023Status musí být "Updated" pro potvrzení, že cert servisování proběhlo.
|
|
# Pokud klíč zcela chybí (starý build / BeforeServicing), netlumíme — BUILD_OUTDATED to zachytí jinak.
|
|
$statusUpdated = ($Reg.UEFICA2023StatusText -eq 'Updated')
|
|
$statusAbsent = ($null -eq $Reg.UEFICA2023Status)
|
|
$certServicingDone = $statusUpdated -or $statusAbsent
|
|
|
|
$ph=[ordered]@{}
|
|
function _p($req,$done,$label){ [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['CertServicingStatus'] = _p $true $certServicingDone 'Certifikáty aktualizovány (UEFICA2023Status=Updated)'
|
|
$ph['BootManager2023'] = _p $true $bmActive 'Boot Manager aktivní (bootuje se z 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é)'
|
|
|
|
$kekDb = $ph['Kek2023'].Done -and $ph['Db2023Windows'].Done
|
|
$arrow = [char]0x2190
|
|
foreach ($key in $ph.Keys) {
|
|
$p=$ph[$key]
|
|
if ($p.Done) { $p.State='Done'; continue }
|
|
if (-not $p.Req) { $p.State='Info'; continue }
|
|
switch ($key) {
|
|
'Kek2023' {
|
|
if ($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' }
|
|
}
|
|
'CertServicingStatus' {
|
|
$p.State = 'Pending'
|
|
if ($Reg.UEFICA2023StatusText -eq 'InProgress') {
|
|
$p.Note = 'servisování probíhá — task ještě nedoběhl nebo čeká na restart'
|
|
} elseif ($Reg.UEFICA2023StatusText -eq 'NotStarted') {
|
|
$p.Note = 'task zatím nezahájil servisování certifikátů'
|
|
} elseif ($Reg.UEFICA2023StatusText -eq 'KeyNotPresent') {
|
|
$p.Note = 'hodnota UEFICA2023Status není nastavena (starý build nebo před první remediací)'
|
|
} else {
|
|
$p.Note = "UEFICA2023Status: $($Reg.UEFICA2023StatusText)"
|
|
}
|
|
}
|
|
'BootManager2023' {
|
|
if ($bmStaged -or ($null -ne $cap -and [int]$cap -ge 1)) {
|
|
$p.State='Pending'; $p.Note="$arrow cert v DB (Capable=$cap) — aktivuje se RESTARTEM"
|
|
} elseif ($kekDb) {
|
|
$p.State='Pending'; $p.Note="$arrow zbývá — vyžaduje RESTART"
|
|
} else {
|
|
$p.State='Pending'; $p.Note='až po nasazení KEK + DB'
|
|
}
|
|
}
|
|
default {
|
|
if ($e1795) { $p.State='Fail'; $p.Note='chyba firmwaru (Event 1795)' } else { $p.State='Pending'; $p.Note='zbývá nasadit' }
|
|
}
|
|
}
|
|
}
|
|
return $ph
|
|
}
|
|
|
|
function Get-RemediationCategory {
|
|
param($Result)
|
|
$sb=$Result.SecureBoot; $reg=$Result.Registry; $evt=$Result.EventLog
|
|
$env=$Result.EnvironmentType; $ph=$Result.Phases; $cert=$Result.Certificates; $mode=$Result.OperatingMode
|
|
|
|
# ── Základní blokátory ────────────────────────────────────────────────────
|
|
if (-not $sb.IsUEFI -or -not $sb.IsSupported) {
|
|
$lbl = if ($env -like '*VM*') { 'Secure Boot nepodporováno (VM bez vTPM/UEFI)' } else { 'Secure Boot nepodporováno (Legacy BIOS)' }
|
|
$cod = if ($env -like '*VM*') { 'NO_SECUREBOOT_VM' } else { 'NO_SECUREBOOT' }
|
|
return @{ Code=$cod; Tag='[X]'; Color='DarkGray'; Label=$lbl }
|
|
}
|
|
if (-not $sb.IsEnabled) {
|
|
return @{ Code='SECUREBOOT_DISABLED'; Tag='[OFF]'; Color='Yellow'; Label='Secure Boot vypnuto' }
|
|
}
|
|
if ($mode -eq 'Setup') {
|
|
return @{ Code='SETUP_MODE'; Tag='[!]'; Color='Magenta'; Label='Secure Boot v Setup Mode — aktualizaci nelze dokončit (chybí enrolled PK)' }
|
|
}
|
|
if (-not $ph['TaskExists'].Done) {
|
|
return @{ Code='TASK_MISSING'; Tag='[!]'; Color='Magenta'; Label='Chybí servicing task — nainstalujte aktuální kumulativní update (min. build 10/2025)' }
|
|
}
|
|
|
|
# ── Chyby a blokování ─────────────────────────────────────────────────────
|
|
$errEvt = $reg.UEFICA2023ErrorEvent -and ([int]$reg.UEFICA2023ErrorEvent -ne 0)
|
|
if ($evt.ById[1795] -or $errEvt -or $reg.UEFICA2023StatusText -eq 'Failed') {
|
|
return @{ Code='UPDATE_FAILED'; Tag='[FAIL]'; Color='Red'; Label='Selhání aktualizace (chyba firmwaru / UEFICA2023ErrorEvent)' }
|
|
}
|
|
if ($reg.ConfidenceLevel -like 'Not Supported*') {
|
|
return @{ Code='NOT_SUPPORTED'; Tag='[X]'; 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'; Tag='[FW]'; Color='Magenta'; Label='Aktualizace pozastavena (známý problém) — zkontrolujte firmware u OEM' }
|
|
}
|
|
|
|
# ── Starý build: certy jsou v KEK/DB, ale chybí servicing infrastruktura pro Boot Manager ──
|
|
# WindowsUEFICA2023Capable i UEFICA2023Status zcela chybí → Windows Update nutný.
|
|
$certApplied = $ph['Kek2023'].Done -or $ph['Db2023Windows'].Done
|
|
$auNow = [int]$reg.AvailableUpdates
|
|
$bmStaged = ([bool]$Result.BootManager.EspHas2023) -or ($auNow -eq 0x4100) -or ($auNow -eq 0x4000)
|
|
if ($certApplied -and -not $ph['BootManager2023'].Done -and -not $bmStaged `
|
|
-and ($null -eq $reg.WindowsUEFICA2023Capable) -and ($null -eq $reg.UEFICA2023Status)) {
|
|
return @{ Code='BUILD_OUTDATED'; Tag='[!]'; Color='Magenta'; Label='Build je příliš starý — chybí servicing pro Boot Manager 2023. Nutný Windows Update.' }
|
|
}
|
|
|
|
# ── Fázové vyhodnocení ────────────────────────────────────────────────────
|
|
$req = @('Kek2023','Db2023Windows','CertServicingStatus','BootManager2023')
|
|
$missing = @($req | 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 -or [bool]$Result.BootManager.EspHas2023
|
|
|
|
if ($missing.Count -eq 0) {
|
|
if ($cert.AnyExpiring2011) {
|
|
return @{ Code='OK_TRANSITION'; Tag="[$SYM_DONE]"; Color='Green'
|
|
Label='HOTOVO — 2023 certifikáty i aktivní Boot Manager (staré 2011 ještě přítomné, což je normální)' }
|
|
}
|
|
return @{ Code='OK'; Tag="[$SYM_DONE]"; Color='Green'; Label='HOTOVO — kompletní 2023 sada, aktivní Boot Manager, 2011 odstraněny' }
|
|
}
|
|
if ($missing.Count -eq 1 -and $missing[0] -eq 'BootManager2023') {
|
|
return @{ Code='UPDATE_PENDING'; Tag='[~]'; Color='Yellow'
|
|
Label='UEFICA2023Status=Updated, KEK i DB hotové — zbývá aktivovat Boot Manager: vyžaduje RESTART' }
|
|
}
|
|
if ($anyApplied) {
|
|
return @{ Code='UPDATE_PARTIAL'; Tag='[~]'; Color='Cyan'
|
|
Label='Probíhá po částech — zbývá: ' + ($missingLabels -join '; ') }
|
|
}
|
|
return @{ Code='UPDATE_NEEDED'; Tag='[!]'; Color='Yellow'; Label='Nutná aktualizace certifikátů' }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Detekce — orchestrace ──────────────────────────────────────────────
|
|
|
|
function Invoke-Detection {
|
|
param([bool]$SkipBootMgrFile)
|
|
|
|
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
|
|
$osBuildFull = try {
|
|
$cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
|
|
'{0}.{1}' -f $cv.CurrentBuildNumber, $cv.UBR
|
|
} catch {
|
|
if ($os) { [string]$os.BuildNumber } else { '?' }
|
|
}
|
|
|
|
$R = [ordered]@{
|
|
AuditTimestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
|
Hostname = $env:COMPUTERNAME
|
|
OSCaption = if ($os) { $os.Caption } else { $null }
|
|
OSBuildFull = $osBuildFull
|
|
EnvironmentType = Get-EnvironmentType
|
|
Hardware = Get-HardwareInfo
|
|
SecureBoot = Get-SecureBootState
|
|
OperatingMode = 'Unknown'
|
|
BitLocker = $null
|
|
Certificates = $null
|
|
Registry = Get-RegistryStatus
|
|
EventLog = Get-EventLogStatus
|
|
TaskExists = Get-TaskExists
|
|
BootManager = $null
|
|
Phases = $null
|
|
Category = $null
|
|
CategoryLabel = $null
|
|
CategoryColor = $null
|
|
}
|
|
|
|
if ($R.SecureBoot.IsUEFI -and $R.SecureBoot.IsSupported) {
|
|
$R.OperatingMode = Get-SecureBootMode
|
|
$R.Certificates = Get-CertificateStatus
|
|
$R.BootManager = Get-BootManagerStatus -SkipFileCheck:$SkipBootMgrFile
|
|
} else {
|
|
$R.Certificates = [ordered]@{
|
|
KEK = [ordered]@{ Has2011=$false; Has2023=$false; Certs2011=@(); Certs2023=@(); Error='SB nedostupný' }
|
|
DB = [ordered]@{ Has2011UEFI=$false; Has2011WindowsPCA=$false; Has2023UEFI=$false
|
|
Has2023OptionROM=$false; Has2023WindowsUEFI=$false
|
|
Certs2011=@(); Certs2023=@(); Error='SB nedostupný' }
|
|
DbxRevokesPCA2011=$false; AnyExpiring2011=$false
|
|
}
|
|
$R.BootManager = [ordered]@{ EspHas2023=$false; Evidence=@(); Chain=$null; Signer=$null; Error='SB nedostupný'; Checked=$false }
|
|
}
|
|
|
|
$R.BitLocker = Get-BitLockerInfo
|
|
$R.Phases = Build-Phases -Sb $R.SecureBoot -Cert $R.Certificates -Boot $R.BootManager `
|
|
-TaskExists $R.TaskExists -Reg $R.Registry -Evt $R.EventLog
|
|
$cat = Get-RemediationCategory -Result $R
|
|
$R.Category = $cat.Code
|
|
$R.CategoryLabel = "$($cat.Tag) $($cat.Label)"
|
|
$R.CategoryColor = $cat.Color
|
|
return $R
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Výpis ──────────────────────────────────────────────────────────────
|
|
|
|
function Show-DetectionSummary {
|
|
param($R, $Prereqs, $Prev)
|
|
$sb=$R.SecureBoot; $c=$R.Certificates; $reg=$R.Registry; $evt=$R.EventLog; $hw=$R.Hardware; $ph=$R.Phases; $bl=$R.BitLocker
|
|
|
|
Write-Head 'SERVER'
|
|
Write-KV 'Stroj' $R.Hostname 'White' ("· {0} (build {1})" -f $R.OSCaption, $R.OSBuildFull)
|
|
Write-KV 'Prostředí' $R.EnvironmentType 'White' ("· {0} {1}" -f $hw.Manufacturer, $hw.Model)
|
|
if ($sb.IsUEFI -and $sb.IsSupported) {
|
|
$modeColor = if ($R.OperatingMode -eq 'Setup') { 'Red' } elseif ($R.OperatingMode -in @('User','Deployed')) { 'White' } else { 'Yellow' }
|
|
$modeNote = if ($R.OperatingMode -eq 'Setup') { '· Setup Mode = nelze dokončit!' } else { $null }
|
|
Write-KV 'Secure Boot' ("zapnuto · mode {0}" -f $R.OperatingMode) $modeColor $modeNote
|
|
}
|
|
if ($bl -and $bl.Status -in @('On','Off','1','2','0')) {
|
|
$blOn = ($bl.Status -eq 'On' -or $bl.Status -eq '1' -or $bl.Status -eq '2')
|
|
$blRisk = $blOn -and $bl.UsesPcr
|
|
$blNote = if ($blRisk) { '· PCR/TPM — před restartem ověř recovery key!' } elseif ($blOn) { '· chráněno' } else { '' }
|
|
$blColor = if ($blRisk) { 'Yellow' } else { 'White' }
|
|
$noteColor = if ($blRisk) { 'Yellow' } else { 'DarkGray' }
|
|
Write-KV 'BitLocker' ("{0} [{1}]" -f $bl.Status, $bl.Protectors) $blColor $blNote $noteColor
|
|
}
|
|
|
|
if ($Prereqs) { Show-Prerequisites -Prereqs $Prereqs }
|
|
|
|
Write-Head 'POSTUP AKTUALIZACE (checklist)'
|
|
if ($sb.IsUEFI -and $sb.IsSupported) { foreach ($k in $ph.Keys) { $p=$ph[$k]; Write-Check -State $p.State -Text $p.Label -Note $p.Note } }
|
|
else { Write-Check -State 'Fail' -Text 'Secure Boot není podporováno / zapnuto' -Note 'remediace zde nedává smysl' }
|
|
|
|
Show-Progress -R $R -Prev $Prev
|
|
|
|
Write-Head 'STAV REGISTRŮ / FIRMWARE'
|
|
Write-KV 'AvailableUpdates' (Get-AvailableUpdatesText $reg.AvailableUpdates) 'Cyan'
|
|
Write-KV 'UEFICA2023Status' $reg.UEFICA2023StatusText $(if($reg.UEFICA2023StatusText -eq 'Updated'){'Green'}elseif($reg.UEFICA2023StatusText -eq 'Failed'){'Red'}else{'White'})
|
|
Write-KV 'WinUEFICA2023Capable' $reg.WindowsUEFICA2023CapableText $(if($reg.WindowsUEFICA2023Capable -eq 2){'Green'}else{'White'})
|
|
if ($reg.ConfidenceLevel) { Write-KV 'ConfidenceLevel' $reg.ConfidenceLevel $(if($reg.ConfidenceLevel -like 'High*'){'Green'}elseif($reg.ConfidenceLevel -like 'Not Supported*'){'Red'}else{'Yellow'}) }
|
|
if ($null -ne $reg.UEFICA2023ErrorEvent -and [int]$reg.UEFICA2023ErrorEvent -ne 0) { Write-KV 'UEFICA2023ErrorEvent' ([string]$reg.UEFICA2023ErrorEvent) 'Red' '· Event ID poslední chyby' }
|
|
if ($R.BootManager.Error -and -not $SkipBootManagerFileCheck) { Write-KV 'Boot Manager (ESP)' 'neověřeno souborově' 'Yellow' ("· {0}" -f $R.BootManager.Error) }
|
|
elseif ($R.BootManager.EspHas2023) { Write-KV 'Boot Manager (ESP)' 'podepsán CA 2023 (staged)' 'Green' }
|
|
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)){'Red'}else{'White'}) ("· {0}" -f $evt.LastEventTime) }
|
|
Write-Host ' cílový stav: AvailableUpdates=0x0 · UEFICA2023Status=Updated · WinUEFICA2023Capable=2' -ForegroundColor DarkGray
|
|
|
|
Write-Host ''
|
|
Write-Host ' VÝSLEDEK: ' -ForegroundColor Gray -NoNewline
|
|
Write-Host $R.CategoryLabel -ForegroundColor $R.CategoryColor
|
|
Add-LogLine ("VÝSLEDEK: {0}" -f $R.CategoryLabel)
|
|
}
|
|
|
|
function Show-DetailedDetection {
|
|
param($R)
|
|
$c=$R.Certificates; $evt=$R.EventLog; $bm=$R.BootManager; $reg=$R.Registry
|
|
Write-Head 'PODROBNOSTI'
|
|
foreach ($g in @(@{N='KEK';O=$c.KEK}, @{N='DB';O=$c.DB})) {
|
|
foreach ($ci in (@($g.O.Certs2011)+@($g.O.Certs2023))) {
|
|
Write-Host (" [{0}] {1}" -f $g.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 ($null -ne $reg.AvailableUpdates) {
|
|
Write-Host ' AvailableUpdates — naplánované operace (bit):' -ForegroundColor DarkGray
|
|
$iv=[int]$reg.AvailableUpdates
|
|
foreach ($b in $AU_BITS) { if ($iv -band $b.Bit) { Write-Host (" 0x{0:X4} {1}" -f $b.Bit, $b.Name) -ForegroundColor Yellow } }
|
|
}
|
|
if ($evt.RelevantEvents.Count) {
|
|
Write-Host ' Poslední události:' -ForegroundColor DarkGray
|
|
foreach ($e in ($evt.RelevantEvents|Select-Object -First 6)) {
|
|
$ec=if($e.EventId -in @(1808,1799)){'Green'}elseif($e.EventId -in @(1795,1796)){'Red'}else{'DarkGray'}
|
|
Write-Host (" [{0}] EventID {1} {2}" -f $e.TimeCreated, $e.EventId, $e.Level) -ForegroundColor $ec
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Plán remediace + souhlas ───────────────────────────────────────────
|
|
|
|
function Resolve-RemediationPlan {
|
|
param($R, [bool]$ForceMode)
|
|
$plan=[ordered]@{ Applicable=$false; Caution=$null; Reason=$null; NextSteps=@() }
|
|
switch -Wildcard ($R.Category) {
|
|
'OK' { $plan.Reason='Server je kompletně hotový — žádná akce.'; if ($ForceMode){ $plan.Applicable=$true } }
|
|
'OK_TRANSITION' {
|
|
$plan.Reason='2023 certifikáty i aktivní Boot Manager jsou nasazeny. Staré 2011 zůstávají (normální). Žádná akce.'
|
|
$plan.NextSteps=@('Nepovinné: časem ověřte revokaci starého boot manageru (DBX).')
|
|
if ($ForceMode){ $plan.Applicable=$true }
|
|
}
|
|
'UPDATE_NEEDED' { $plan.Applicable=$true; $plan.Reason='Lze zahájit aktualizaci metodou registry (KB5068202).' }
|
|
'UPDATE_PARTIAL' { $plan.Applicable=$true; $plan.Reason='Část už nasazena, zbytek se aplikuje v dalším cyklu.'; $plan.Caution='Po nastavení a tasku bude potřeba RESTART; opakujte, dokud nebude HOTOVO.' }
|
|
'UPDATE_PENDING' {
|
|
$au = [int]$R.Registry.AvailableUpdates
|
|
# 0x4100 = Boot Manager je staged na ESP — stačí RESTART, task nespouštět.
|
|
# Vše ostatní (0x4000, 0x0, …) — spustit task, který posune proces dál.
|
|
if ($au -eq 0x4100) {
|
|
$plan.Reason = 'Boot Manager 2023 je staged (AvailableUpdates=0x4100). Aktivuje se RESTARTEM — task spouštět netřeba.'
|
|
$plan.NextSteps = @('RESTARTUJTE server.', 'BitLocker (PCR7): před restartem ověřte recovery key.', 'Po restartu spusťte kontrolu znovu.')
|
|
} else {
|
|
$plan.Applicable = $true
|
|
$plan.Reason = ('KEK i DB hotové, Boot Manager v procesu (AvailableUpdates=0x{0:X}). Task posune proces dál.' -f $au)
|
|
$plan.Caution = 'Po dokončení tasku bude nutný RESTART pro aktivaci Boot Manageru.'
|
|
$plan.NextSteps = @('Po dokončení tasku RESTARTUJTE server.', 'Po restartu spusťte kontrolu znovu.')
|
|
}
|
|
}
|
|
'UPDATE_FAILED' {
|
|
if ($R.EventLog.ById[1795]) {
|
|
$plan.Reason='Selhání Event 1795 (chyba firmwaru). Příčina je ve firmwaru/hostiteli.'
|
|
$plan.NextSteps=@('Aktualizujte firmware / Hyper-V hostitele (KB5085790).','Poté spusťte kontrolu znovu.')
|
|
if ($ForceMode){ $plan.Applicable=$true; $plan.Caution='Event 1795 = problém firmwaru; -Force jen zopakuje pokus.' }
|
|
} else { $plan.Applicable=$true; $plan.Reason='Předchozí pokus selhal. Lze zopakovat.'; $plan.Caution='Zkontrolujte UEFICA2023ErrorEvent výše.' }
|
|
}
|
|
'FIRMWARE_UPDATE_NEEDED' {
|
|
$plan.Reason='Aktualizace pozastavena (Temporarily Paused / Event 1802) — známý problém firmwaru.'
|
|
$plan.NextSteps=@('Aktualizujte firmware u výrobce (Dell/HP/Lenovo/Supermicro).','Poté spusťte kontrolu znovu.')
|
|
if ($ForceMode){ $plan.Applicable=$true; $plan.Caution='Pozastaveno firmwarem; -Force se pokusí i tak.' }
|
|
}
|
|
'NOT_SUPPORTED' {
|
|
$plan.Reason='ConfidenceLevel: Not Supported — zařízení nepodporuje automatickou cestu.'
|
|
$plan.NextSteps=@('Ověřte firmware update u OEM.','Pokud není, dokumentujte jako trvalou výjimku.')
|
|
}
|
|
'SETUP_MODE' {
|
|
$plan.Reason='Secure Boot je v Setup Mode (chybí enrolled Platform Key) — aktualizaci nelze dokončit.'
|
|
$plan.NextSteps=@('V UEFI/BIOS obnovte výchozí Secure Boot klíče (enroll PK) / přepněte do User Mode.','Poté spusťte kontrolu znovu.')
|
|
}
|
|
'BUILD_OUTDATED' {
|
|
$plan.Reason='OS má 2023 certifikáty v DB/KEK, ale chybí servicing pro aktivaci 2023 Boot Manageru (WindowsUEFICA2023Capable i UEFICA2023Status jsou prázdné). Boot Manager je stále podepsaný 2011 a tento build neumí nasadit nový — restarty ani remediace nepomohou.'
|
|
$plan.NextSteps=@('Nainstalujte nejnovější kumulativní update Windows (Windows Update) a restartujte.','Poté spusťte kontrolu znovu — pak by se Boot Manager měl dát aktualizovat.','Pozn.: velmi staré buildy (např. Server 2016 RTM) neobsahují 2023 boot-manager servicing.')
|
|
}
|
|
'TASK_MISSING' {
|
|
$plan.Reason='Servicing task neexistuje — typicky chybí build z 10/2025 (KB5066835).'
|
|
$plan.NextSteps=@('Nainstalujte nejnovější kumulativní update Windows.','Poté spusťte kontrolu znovu.')
|
|
}
|
|
'SECUREBOOT_DISABLED' { $plan.Reason='Secure Boot je vypnutý — zapnutí je rozhodnutí mimo rozsah skriptu.'; $plan.NextSteps=@('Zvažte zapnutí Secure Boot (pozor na BitLocker PCR7).') }
|
|
'NO_SECUREBOOT*' { $plan.Reason='Secure Boot není podporováno (Legacy BIOS / VM bez vTPM).'; $plan.NextSteps=@('Dokumentujte jako výjimku.') }
|
|
default { $plan.Reason='Stav nebylo možné jednoznačně vyhodnotit.' }
|
|
}
|
|
return $plan
|
|
}
|
|
|
|
function Get-UserConsent {
|
|
param([string]$Question, [string]$Detail)
|
|
if ($AssumeYes) { Write-Line (" {0} -> automaticky ANO (-AssumeYes)" -f $Question) Cyan; return $true }
|
|
if (-not [Environment]::UserInteractive) { Write-Line ' Neinteraktivní relace bez -AssumeYes — remediace se neprovede.' Yellow; return $false }
|
|
Write-Host ''; Write-Host $Question -ForegroundColor White
|
|
if ($Detail) { Write-Host $Detail -ForegroundColor DarkGray }
|
|
try {
|
|
$yes=New-Object System.Management.Automation.Host.ChoiceDescription '&Ano','Aplikovat (bez restartu)'
|
|
$no =New-Object System.Management.Automation.Host.ChoiceDescription '&Ne','Neprovádět změny'
|
|
return ($Host.UI.PromptForChoice('','Vaše volba:',[System.Management.Automation.Host.ChoiceDescription[]]@($yes,$no),1) -eq 0)
|
|
} catch { return ((Read-Host 'Aplikovat? [a/N]') -match '^(a|ano|y|yes)$') }
|
|
}
|
|
|
|
function Invoke-RestartOffer {
|
|
# Nabídne uživateli restart. Skript nikdy nerestartuje automaticky — vždy interaktivní dotaz.
|
|
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 Read-Host)'
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Stav napříč restarty + RunOnce ─────────────────────────────────────
|
|
|
|
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 -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 } }
|
|
function Register-Resume {
|
|
$sp=Get-ScriptPath; if (-not $sp) { Write-Line ' (Nelze zjistit cestu skriptu — auto-recheck nenastaven.)' DarkGray; return }
|
|
$cmd='powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{0}" -CheckOnly' -f $sp
|
|
try {
|
|
if (-not (Test-Path $REG_RUNONCE)) { New-Item -Path $REG_RUNONCE -Force | Out-Null }
|
|
New-ItemProperty -Path $REG_RUNONCE -Name 'SecureBootCA2023Recheck' -Value $cmd -PropertyType String -Force | Out-Null
|
|
Write-Line ' Auto-recheck po příštím restartu nastaven (RunOnce, jen kontrola — žádný restart).' Green
|
|
} catch { Write-Line (" (Auto-recheck se nepodařilo nastavit: {0})" -f $_.Exception.Message) DarkGray }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Remediace ──────────────────────────────────────────────────────────
|
|
|
|
function Invoke-Remediation {
|
|
param([bool]$SkipBootMgrFile)
|
|
$out=[ordered]@{ Status='Unknown'; Message=''; After=$null }
|
|
Write-Head 'REMEDIACE'; Add-LogLine 'REMEDIACE START'
|
|
|
|
# AvailableUpdates se nastavuje na 0x5944 POUZE jednou — při prvním spuštění (null) nebo po
|
|
# dokončení předchozího cyklu (0). Systém si pak hodnotu sám spravuje: odečítá bity jak postupuje
|
|
# (0x5944→0x5904→0x5104→0x4104→0x4100→0x4000→0x0). Skript ji neresetuje — jen čte a interpretuje.
|
|
$auCurrent = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue).AvailableUpdates
|
|
$auNeedsInit = ($null -eq $auCurrent) -or ([int]$auCurrent -eq 0)
|
|
$auInfo = if ($auNeedsInit) { '0x5944 (první inicializace)' } else { '0x{0:X} zachováno (probíhá)' -f [int]$auCurrent }
|
|
Write-Host (" [1/3] Nastavuji registry (MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}) ... " -f $auInfo) -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
|
|
$vCheck = (Get-ItemProperty $REG_SECUREBOOT -ErrorAction Stop).AvailableUpdates
|
|
if ([int]$vCheck -ne $AVAILABLE_UPDATES_VALUE) { throw ('Ověření selhalo — AvailableUpdates=0x{0:X}, očekáváno 0x5944' -f [int]$vCheck) }
|
|
}
|
|
$o = Get-ItemProperty $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -ErrorAction SilentlyContinue
|
|
if ($o -and $o.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 'hotovo' -ForegroundColor Green
|
|
$auLogVal = if ($auNeedsInit) { '0x5944 (nastaveno)' } else { '0x{0:X} (zachováno)' -f [int]$auCurrent }
|
|
Add-LogLine ("Krok 1: MicrosoftUpdateManagedOptIn=1, AvailableUpdates={0}" -f $auLogVal)
|
|
} catch { Write-Host 'CHYBA' -ForegroundColor Red; Write-Line (" {0}" -f $_.Exception.Message) Red; $out.Status='Error'; $out.Message="Zápis registry selhal: $($_.Exception.Message)"; return $out }
|
|
|
|
if ($SkipScheduledTask) { Write-Line ' [2/3] Servicing task přeskočen (-SkipScheduledTask).' DarkGray }
|
|
else {
|
|
Write-Host ' [2/3] Spouštím servicing task a čekám na změnu stavu ' -NoNewline
|
|
$task=Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue
|
|
if (-not $task) { Write-Host '— task nenalezen' -ForegroundColor Yellow; Write-Line ' Registry nastavena; Windows ji zpracuje při servisním běhu.' DarkGray }
|
|
else {
|
|
$init=[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
|
|
$el=0; $state='Running'; $changed=$false
|
|
do {
|
|
Start-Sleep -Seconds 3; $el+=3; Write-Host '.' -NoNewline -ForegroundColor DarkGray
|
|
$state=(Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue).State
|
|
if ([int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates) -ne $init) { $changed=$true; break }
|
|
} while ($state -eq 'Running' -and $el -lt $TASK_TIMEOUT_SEC)
|
|
$now=[int]((Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction SilentlyContinue).AvailableUpdates)
|
|
if ($changed) { Write-Host (" hotovo ({0}s, AvailableUpdates -> 0x{1:X})" -f $el,$now) -ForegroundColor Green }
|
|
elseif ($el -ge $TASK_TIMEOUT_SEC) { Write-Host (" timeout {0}s (stav {1})" -f $el,$state) -ForegroundColor Yellow }
|
|
else { Write-Host (" hotovo ({0}s, stav {1})" -f $el,$state) -ForegroundColor Green }
|
|
Add-LogLine ("Krok 2: stav=$state, elapsed=${el}s, AvailableUpdates=0x{0:X}" -f $now)
|
|
} catch { Write-Host '— nepodařilo se spustit' -ForegroundColor Yellow; Write-Line (" {0}" -f $_.Exception.Message) DarkGray }
|
|
}
|
|
}
|
|
|
|
Write-Host ' [3/3] Ověřuji nový stav (vč. Boot Manageru) ... ' -NoNewline
|
|
Start-Sleep -Seconds 2
|
|
$after=Invoke-Detection -SkipBootMgrFile:$SkipBootMgrFile
|
|
Write-Host 'hotovo' -ForegroundColor Green; Write-Host ''
|
|
Write-Line (' AvailableUpdates: {0}' -f (Get-AvailableUpdatesText $after.Registry.AvailableUpdates)) Cyan
|
|
Write-Line (' UEFICA2023Status : {0}' -f $after.Registry.UEFICA2023StatusText) $(if($after.Registry.UEFICA2023StatusText -eq 'Updated'){'Green'}else{'White'})
|
|
foreach ($k in @('Kek2023','Db2023Windows','CertServicingStatus','BootManager2023')) { $p=$after.Phases[$k]; Write-Check -State $p.State -Text $p.Label -Note $p.Note }
|
|
$afterAU = [int]$after.Registry.AvailableUpdates
|
|
if ($afterAU -eq 0x4100) {
|
|
Write-Host ''
|
|
Write-Host ' *** Boot Manager 2023 je staged (AvailableUpdates=0x4100) ***' -ForegroundColor Yellow
|
|
Write-Host ' Aktivace proběhne po restartu serveru.' -ForegroundColor Yellow
|
|
Add-LogLine 'Post-remediace: AU=0x4100 — BM staged, vyžaduje restart'
|
|
Invoke-RestartOffer
|
|
}
|
|
$out.Status='Applied'; $out.After=$after; $out.Message='Registry nastavena, task zpracován. Boot Manager se aktivuje až po restartu.'
|
|
Add-LogLine 'REMEDIACE KONEC: Applied'
|
|
return $out
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Předpoklady + progress ────────────────────────────────────────────
|
|
|
|
function Get-StageInfo {
|
|
param($v)
|
|
if ($null -eq $v) { return @{ Step=0; Total=6; Label='nezahájeno' } }
|
|
switch ([int]$v) {
|
|
0x5944 { return @{ Step=1; Total=6; Label='naplánováno (start)' } }
|
|
0x5904 { return @{ Step=2; Total=6; Label='Windows UEFI CA 2023 v DB' } }
|
|
0x5104 { return @{ Step=3; Total=6; Label='Option ROM UEFI CA 2023 v DB' } }
|
|
0x4104 { return @{ Step=4; Total=6; Label='Microsoft UEFI CA 2023 v DB' } }
|
|
0x4100 { return @{ Step=5; Total=6; Label='Boot Manager nasazen na ESP — RESTART' } }
|
|
0x4000 { return @{ Step=6; Total=6; Label='guard bit — task dokončuje' } }
|
|
0 { return @{ Step=6; Total=6; Label='dokončeno' } }
|
|
default { return @{ Step=1; Total=6; Label=('mezistav 0x{0:X}' -f [int]$v) } }
|
|
}
|
|
}
|
|
|
|
function Show-Progress {
|
|
param($R, $Prev)
|
|
if ($R.Category -notlike 'UPDATE*' -and $R.Category -notlike 'OK*') { return }
|
|
Write-Head 'PRŮBĚH (krok a změna od minulého běhu)'
|
|
$st = Get-StageInfo $R.Registry.AvailableUpdates
|
|
$bar = ('#' * $st.Step) + ('.' * ($st.Total - $st.Step))
|
|
Write-Host (" [{0}] krok {1}/{2} — {3}" -f $bar, $st.Step, $st.Total, $st.Label) -ForegroundColor Cyan
|
|
if ($Prev) {
|
|
$pv = [string]$Prev.AvailableUpdates
|
|
$cv = '0x{0:X}' -f [int]$R.Registry.AvailableUpdates
|
|
if ($pv -eq $cv) {
|
|
Write-Host (" Beze změny od minulého běhu (#{0}, {1})." -f $Prev.Cycle, $Prev.Timestamp) -ForegroundColor Yellow
|
|
if ($R.Category -in @('UPDATE_PENDING','UPDATE_PARTIAL')) {
|
|
Write-Host ' -> Proces čeká na RESTART. Pokud jste restartoval, spusťte skript znovu; jinak server restartujte.' -ForegroundColor Yellow
|
|
}
|
|
} else {
|
|
Write-Host (" Od minulého běhu: AvailableUpdates {0} -> {1} (postup)" -f $pv, $cv) -ForegroundColor Green
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-Prerequisites {
|
|
param($R, [bool]$IsAdmin)
|
|
$sb = $R.SecureBoot
|
|
$list = @()
|
|
|
|
$list += @{ Label='Spuštěno jako Administrator'
|
|
Ok=$IsAdmin; Hard=$true
|
|
Note=if(-not $IsAdmin){'spusťte PowerShell „Run as administrator"'} }
|
|
|
|
$sbOk = $sb.IsUEFI -and $sb.IsSupported
|
|
$list += @{ Label='UEFI + Secure Boot podporováno'
|
|
Ok=$sbOk; Hard=$true
|
|
Note=if(-not $sbOk){'Legacy BIOS / nepodporováno'} }
|
|
|
|
$list += @{ Label='Secure Boot zapnutý'
|
|
Ok=[bool]$sb.IsEnabled; Hard=$true
|
|
Note=if($sbOk -and -not $sb.IsEnabled){'zapněte v UEFI'} }
|
|
|
|
$list += @{ Label='Ne Setup Mode (enrolled PK)'
|
|
Ok=($R.OperatingMode -ne 'Setup'); Hard=$true
|
|
Note=if($R.OperatingMode -eq 'Setup'){'obnovte Secure Boot klíče v UEFI'} }
|
|
|
|
$buildNote = if ($R.TaskExists) {
|
|
"build $($R.OSBuildFull)"
|
|
} else {
|
|
"build $($R.OSBuildFull) — chybí task, nainstalujte 10/2025+ (KB5066835)"
|
|
}
|
|
$list += @{ Label='Build s podporou aktualizace (servicing task)'
|
|
Ok=[bool]$R.TaskExists; Hard=$true; Note=$buildNote }
|
|
|
|
# Starý build: certy jsou v KEK/DB, ale WindowsUEFICA2023Capable + UEFICA2023Status zcela chybí
|
|
# a BM není staged → servicing infrastruktura pro Boot Manager neexistuje, nutný Windows Update.
|
|
$auP = [int]$R.Registry.AvailableUpdates
|
|
$bmStagedP = ([bool]$R.BootManager.EspHas2023) -or ($auP -eq 0x4100) -or ($auP -eq 0x4000)
|
|
$staleServ = ($R.Phases['Kek2023'].Done -or $R.Phases['Db2023Windows'].Done) `
|
|
-and (-not $R.Phases['BootManager2023'].Done) `
|
|
-and (-not $bmStagedP) `
|
|
-and ($null -eq $R.Registry.WindowsUEFICA2023Capable) `
|
|
-and ($null -eq $R.Registry.UEFICA2023Status)
|
|
$list += @{ Label='Servicing pro Boot Manager 2023 (aktuální build)'
|
|
Ok=(-not $staleServ); Hard=$true
|
|
Note=if($staleServ){"build $($R.OSBuildFull) je starý — chybí 2023 boot-manager servicing, nutný Windows Update"} }
|
|
|
|
$notSupported = $R.Registry.ConfidenceLevel -like 'Not Supported*'
|
|
$list += @{ Label='Zařízení není „Not Supported"'
|
|
Ok=(-not $notSupported); Hard=$false
|
|
Note=if($notSupported){'ConfidenceLevel: Not Supported — řešení u OEM'} }
|
|
|
|
return $list
|
|
}
|
|
|
|
function Show-Prerequisites {
|
|
param($Prereqs)
|
|
Write-Head 'PŘEDPOKLADY (lze proces dokončit?)'
|
|
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 $p.Label -Note $p.Note -NoteColor $nc
|
|
}
|
|
if (@($Prereqs | Where-Object { $_.Hard -and -not $_.Ok }).Count) {
|
|
Write-Host ' ! Než spustíte aktualizaci, vyřešte výše označené řádky — jinak proces nelze dokončit.' -ForegroundColor Red
|
|
} else {
|
|
Write-Host ' Předpoklady splněny — jakmile spustíte, proces lze dokončit.' -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ── Main ───────────────────────────────────────────────────────────────
|
|
|
|
$isWhatIf = [bool]$WhatIfPreference
|
|
|
|
Write-Host ''
|
|
Write-Rule 'Cyan'
|
|
Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor White
|
|
Write-Host (" {0} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray
|
|
Write-Rule 'Cyan'
|
|
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
|
|
Add-LogLine ('===== SPUŠTĚNO {0} | {1} | admin={2} | CheckOnly={3} AssumeYes={4} Force={5} =====' -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $isAdmin, $CheckOnly.IsPresent, $AssumeYes.IsPresent, $Force.IsPresent)
|
|
if (-not $isAdmin) { Write-Host ''; Write-Host ' ! Neběží jako Administrator — část kontrol a remediace nebude dostupná.' -ForegroundColor Yellow }
|
|
|
|
$prev = Get-ResumeState
|
|
if ($prev) { Write-Host ''; Write-Host (" Navazuji na předchozí běh (cyklus #{0}, {1}, stav {2})." -f $prev.Cycle, $prev.Timestamp, $prev.Category) -ForegroundColor DarkCyan }
|
|
|
|
Write-Host ''; Write-Host ' Zjišťuji stav (vč. ověření Boot Manageru)…' -ForegroundColor DarkGray
|
|
$result = Invoke-Detection -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent
|
|
$prereqs = Get-Prerequisites -R $result -IsAdmin $isAdmin
|
|
# Pořadí: SERVER -> PŘEDPOKLADY -> POSTUP AKTUALIZACE (checklist) -> PRŮBĚH -> registry -> VÝSLEDEK
|
|
Show-DetectionSummary -R $result -Prereqs $prereqs -Prev $prev
|
|
if ($Detailed) { Show-DetailedDetection -R $result }
|
|
|
|
if ($CheckOnly) {
|
|
Write-Host ''; Write-Rule
|
|
$code = switch -Wildcard ($result.Category) {
|
|
'OK*' { 0 } 'UPDATE_NEEDED' { 1 } 'UPDATE_PARTIAL' { 1 } 'UPDATE_PENDING' { 1 } default { 2 } }
|
|
Write-Host (" CHECK: {0} (exit {1})" -f $result.CategoryLabel, $code) -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
|
|
if ($PassThru) { $result }
|
|
exit $code
|
|
}
|
|
|
|
$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent
|
|
Write-Head 'VYHODNOCENÍ'
|
|
if ($plan.Reason) { Write-Host (" {0}" -f $plan.Reason) -ForegroundColor Gray }
|
|
|
|
$cycle = if ($prev) { [int]$prev.Cycle } else { 0 }
|
|
$remediation = $null
|
|
if (-not $plan.Applicable) {
|
|
if ($plan.NextSteps.Count) { Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White; $i=1; foreach ($s in $plan.NextSteps) { Write-Host (" {0}. {1}" -f $i,$s) -ForegroundColor Gray; $i++ } }
|
|
elseif ($result.Category -like 'OK*') { Write-Host ' Není potřeba žádná akce.' -ForegroundColor Green }
|
|
# UPDATE_PENDING na 0x4100: Boot Manager staged — nabídnout restart přímo z detekce
|
|
if ($result.Category -eq 'UPDATE_PENDING' -and -not $CheckOnly -and -not $isWhatIf -and $isAdmin) {
|
|
if ([int]$result.Registry.AvailableUpdates -eq 0x4100) { Invoke-RestartOffer }
|
|
}
|
|
} else {
|
|
if ($plan.Caution) { Write-Host (" Pozor: {0}" -f $plan.Caution) -ForegroundColor Yellow }
|
|
if (-not $isAdmin) { Write-Host ''; Write-Host ' Remediaci nelze provést bez práv Administrator.' -ForegroundColor Yellow }
|
|
elseif ($isWhatIf) {
|
|
Write-Host ''; Write-Host ' -WhatIf — co by remediace udělala (bez změn):' -ForegroundColor Cyan
|
|
Write-Host (" - MicrosoftUpdateManagedOptIn=1, AvailableUpdates=0x{0:X4}, HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE) -ForegroundColor Gray
|
|
if (-not $SkipScheduledTask) { Write-Host ' - spustila by servicing task a počkala na změnu stavu' -ForegroundColor Gray }
|
|
Write-Host ' - server by NErestartovala' -ForegroundColor Gray
|
|
} else {
|
|
$q='Chcete nyní zahájit / pokračovat v aktualizaci Secure Boot certifikátů?'
|
|
$d='Nastaví registry (KB5068202) a spustí servicing task. Server NEBUDE restartován.'
|
|
if (Get-UserConsent -Question $q -Detail $d) {
|
|
$cycle = $cycle + 1
|
|
Add-LogLine ("Cyklus #{0} | {1} | kategorie={2}" -f $cycle, $result.Hostname, $result.Category)
|
|
$remediation = Invoke-Remediation -SkipBootMgrFile:$SkipBootManagerFileCheck.IsPresent
|
|
Flush-Log
|
|
} else { Write-Host ''; Write-Host ' Remediace neprovedena (volba uživatele).' -ForegroundColor Yellow }
|
|
}
|
|
}
|
|
|
|
Write-Host ''; Write-Rule 'Cyan'
|
|
if ($remediation -and $remediation.Status -eq 'Applied') {
|
|
$after=$remediation.After
|
|
if ($after.Category -like 'OK*') { Write-Host (" {0} HOTOVO — server je kompletní." -f $SYM_DONE) -ForegroundColor Green }
|
|
else {
|
|
Write-Host ' ČÁSTEČNĚ HOTOVO — proces pokračuje po restartu.' -ForegroundColor Yellow
|
|
Write-Host ''; Write-Host ' Další kroky:' -ForegroundColor White
|
|
Write-Host ' 1. RESTART serveru (byl nabídnut výše; pokud jste odmítli, restartujte ručně).' -ForegroundColor Gray
|
|
if ($after.BitLocker -and $after.BitLocker.UsesPcr -and ($after.BitLocker.Status -eq 'On' -or $after.BitLocker.Status -eq '1' -or $after.BitLocker.Status -eq '2')) {
|
|
Write-Host ' 2. BitLocker (PCR/TPM) je aktivní — před restartem ověřte recovery key!' -ForegroundColor Yellow
|
|
} else { 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 ' Cíl: AvailableUpdates=0x0 · UEFICA2023Status=Updated · WinUEFICA2023Capable=2.' -ForegroundColor DarkGray
|
|
if ($RegisterResume) { Register-Resume } else { Write-Host ' (Tip: -RegisterResume spustí kontrolu po příštím restartu automaticky.)' -ForegroundColor DarkGray }
|
|
}
|
|
} elseif ($remediation -and $remediation.Status -eq 'Error') { Write-Host (' CHYBA REMEDIACE — {0}' -f $remediation.Message) -ForegroundColor Red }
|
|
else { Write-Host ' KONEC — bez změn na serveru.' -ForegroundColor Cyan }
|
|
Write-Host (" Log: {0}" -f $script:LogFile) -ForegroundColor DarkGray
|
|
Write-Rule 'Cyan'; Write-Host ''
|
|
|
|
# Stav pro příští běh (delta) + jednorázový zápis logu
|
|
$finalR = if ($remediation -and $remediation.After) { $remediation.After } else { $result }
|
|
if ($finalR.Category -like 'OK*') { Clear-ResumeState } else { Save-ResumeState -R $finalR -Cycle $cycle }
|
|
Flush-Log
|
|
|
|
if ($PassThru) { return [pscustomobject]@{ Detection=$result; Plan=$plan; Remediation=$remediation } }
|
|
|
|
#endregion
|